JavaScript in the browser

Load a reading suggestion with Fetch

Build and test a browser suggestion loader with loading, empty, HTTP error, parsing error, and offline states.

We’ll add a suggested article to the reading-list page from the final JavaScript project. This lesson builds just the loading button. Later you can connect the result to the list you save in local storage.

The Fetch API guide covers request options, forms, credentials, caching, streams, and cancellation. Here we need a GET request and a reliable interface when it fails.

Create a local response

In a folder served by your local development server, create suggestions.json:

[
  {
    "title": "JavaScript promises",
    "url": "https://flaviocopes.com/javascript-promises/"
  }
]

Serve the page over HTTP. Opening it as a file: URL can prevent Fetch from reading the JSON. Use your existing dev server, or run npx serve . in this practice folder and open the local URL it prints.

Build the loading button

Create index.html. We keep the message separate from the result so an empty response has somewhere to explain itself:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Reading suggestion</title>
</head>
<body>
  <button id="load" type="button">Load a suggestion</button>
  <p id="status" role="status"></p>
  <div id="result"></div>
  <script type="module" src="app.js"></script>
</body>
</html>

Create app.js. The first await gets the response; the second reads and parses its body:

const button = document.querySelector('#load')
const status = document.querySelector('#status')
const result = document.querySelector('#result')

button.addEventListener('click', async () => {
  button.disabled = true
  status.textContent = 'Loading a suggestion…'
  result.replaceChildren()

  try {
    const response = await fetch('./suggestions.json')
    if (!response.ok) {
      throw new Error(`Could not load suggestions (HTTP ${response.status})`)
    }

    const suggestions = await response.json()
    if (!Array.isArray(suggestions)) {
      throw new Error('Expected a list of suggestions')
    }
    if (suggestions.length === 0) {
      status.textContent = 'No suggestions available'
      return
    }

    const suggestion = suggestions[0]
    if (!suggestion || typeof suggestion.title !== 'string' ||
        typeof suggestion.url !== 'string') {
      throw new Error('Invalid suggestion')
    }
    const url = new URL(suggestion.url)
    if (!['http:', 'https:'].includes(url.protocol)) {
      throw new Error('Unsupported link protocol')
    }

    const link = document.createElement('a')
    link.href = url.href
    link.textContent = suggestion.title
    result.append(link)
    status.textContent = 'Suggestion loaded'
  } catch (error) {
    status.textContent = error.message
  } finally {
    button.disabled = false
  }
})

We use textContent for the title so returned text stays text. URL validation also matters: a link can run code if you allow a javascript: URL.

The finally block enables the button after every outcome, including the early return for an empty array. Clearing the old result prevents a failed request from leaving a stale suggestion on screen.

Verify each outcome

Open DevTools before clicking the button. Inspect the request’s status and raw response in Network, then compare them with the page:

ChangeExpected result
Original JSONA link and Suggestion loaded
Replace the file contents with []No suggestions available, no old link
Rename the JSON fileAn HTTP error message, button enabled
Put broken JSON in the fileA parsing error, button enabled
Replace the array with {}Expected a list of suggestions
Use a javascript: URLUnsupported link protocol, no link
Use Network’s Offline settingA request error, button enabled

Restore the file and network setting after each check. Under network throttling, click repeatedly: only one request should start while the button is disabled.

A 404 still resolves the Fetch promise, which is why we check response.ok. Network failures reject it. Invalid JSON rejects the body-reading promise. All three reach this catch block, but at different steps.

Extend the exercise

Add a Cancel button using AbortController and pass its signal to Fetch. Show Loading cancelled for AbortError, and verify that cancellation also enables the load button. Use the cancellation section of the guide for the API details.

Then connect a successful suggestion to the reading list. Update your state array before rendering, following the same path as a manually added item.

Lesson completed