Lesson · Intermediate

Fetch data and handle every response state

Use Fetch with explicit request contracts, abortable lifetimes, status handling, safe output, and honest loading, success, empty, and failure states.

Published
Last checked

Define the request contract first

A Fetch call crosses a trust and failure boundary. Before writing it, name the method, URL, credentials policy, headers, request schema, maximum size, success statuses, response media type, timeout or cancellation behavior, and what the UI does for loading, empty, invalid, unauthorized, rate-limited, server-error, and offline states.

The browser does not make those product decisions. A fulfilled Fetch promise can contain an HTTP error response, and a successful status can contain malformed or unexpected data. Treat transport, protocol, parsing, and schema as separate checks.

Read a JSON response deliberately

async function loadResources(signal) {
  const response = await fetch('/api/resources', {
    headers: { accept: 'application/json' },
    credentials: 'same-origin',
    signal,
  });

  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }

  const contentType = response.headers.get('content-type') ?? '';
  if (!contentType.includes('application/json')) {
    throw new Error('Expected a JSON response');
  }

  const data = await response.json();
  return validateResourceList(data);
}

response.ok covers statuses from 200 through 299. It does not prove the body matches the application schema. A dedicated validator should reject missing, wrongly typed, oversized, or unexpected values before rendering.

Distinguish failure classes

Fetch rejects for failures such as an invalid request, network error, or abort. It normally resolves for a 404 or 500 response. JSON parsing can reject after a successful response, and validation can reject after parsing. Preserve those boundaries in logs and user messages without exposing sensitive details.

A public message can say “Resources could not be loaded. Try again.” A development diagnostic can record a bounded error category and request reference. Do not log response bodies that may contain personal data, tokens, or server implementation details.

Cancel obsolete work

Requests can outlive the screen or query that started them. Use AbortController when a component is removed, a new search supersedes an old one, or a navigation makes the result irrelevant.

const controller = new AbortController();

loadResources(controller.signal).catch((error) => {
  if (error.name !== 'AbortError') showRecoverableError();
});

// Later, when this result is no longer relevant:
controller.abort();

Aborting prevents stale UI updates and unnecessary work. It is not a server transaction rollback; the server may already have received and processed a write. Idempotency and authorization belong at the authoritative endpoint.

Handle writes and forms honestly

Keep server validation even when the client validates first. Build request data from allowed fields, include appropriate content type, and protect state-changing routes against cross-site abuse according to the authentication design. Never accept a client-supplied price, role, ownership identifier, or success flag as authority.

Disable repeated submission only while the authoritative request is pending, and keep the button label and busy state understandable. On failure, restore the control, preserve valid input, move or announce focus only when needed, and show field errors next to associated controls. Report success only after the server confirms it.

Render data safely

Use textContent for untrusted text. Creating markup from a string can turn data into executable HTML and requires a deliberate sanitization boundary. URLs also need validation: confirm allowed schemes and destinations rather than placing an arbitrary value into href or src.

An empty array is not necessarily an error. Decide whether it means “no matches,” “no records yet,” or an invalid response. Keep loading, empty, error, and success states mutually understandable and expose dynamic status changes without stealing focus unnecessarily.

CORS, credentials, and caching

Same-origin requests are the simplest trust boundary. Cross-origin requests are subject to CORS; the server, not the client, decides which origins may read a response. mode: 'no-cors' does not bypass that policy and produces an opaque response that application code cannot inspect.

Choose credential behavior deliberately. Cookies may carry authority, so a cross-origin credentialed request needs a narrow server policy. Understand cache headers and method semantics before adding client caches. A stale response can be a correctness or privacy defect, not only a performance issue.

Common failures

  • The code calls response.json() without checking status or media type.
  • A 500 response is shown as success because Fetch fulfilled.
  • A slower old search overwrites a newer result.
  • Untrusted fields enter innerHTML or unsafe URLs.
  • A disabled submit button never recovers after rejection.
  • Sensitive request or response data enters analytics and logs.

Verify and practice

Use browser request blocking and throttling. Return 204, 400, 401, 403, 404, 409, 429, 500, HTML instead of JSON, malformed JSON, an empty valid response, and valid JSON with the wrong schema. Start two requests in reverse completion order and confirm the newest intent wins. Navigate away while a request is pending and ensure no stale update appears.

For practice, enhance a server-backed search form. Keep its native GET action, then intercept it only after initialization succeeds. Abort superseded searches, validate the response, render result text safely, update the URL so the state is shareable, and restore the native path when JavaScript is disabled or the enhancement fails.

Free template + launch checklist

Build a page you can check before launch

Start with one reviewed semantic HTML page, then work through the one-page checklist against your published URL.

Check your inbox and confirm before the download arrives. No account required. We do not track email opens. See the privacy notice.