Fetch and Async
Using fetch, async/await, error handling, loading states, and patterns for data loading in the browser.
Loading data from the network is a core frontend task. The Fetch API and async/await make it straightforward—but you still need clear patterns for errors, loading states, and cancellation. This guide covers the essentials and gives you patterns you can reuse in any project.
The Fetch API
fetch(url, options) returns a Promise that resolves with a Response object. The response body isn't parsed yet—you call .json(), .text(), or .blob() to get the data. Fetch only rejects on network failure (e.g. no connection); HTTP errors (404, 500) resolve with response.ok === false. Always check response.ok or response.status and throw or handle errors accordingly.
A minimal GET request looks like this:
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
For POST requests with a JSON body, pass method, headers, and body:
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Jane", role: "developer" }),
});
if (!response.ok) throw new Error("Failed to create user");
const user = await response.json();
Remember: fetch does not throw on 4xx/5xx. You must check response.ok or response.status and throw yourself so that a single try/catch can handle both network errors and bad HTTP statuses.
async/await
Declare a function with async to use await inside it. await pauses until the Promise settles and returns the resolved value. Use try/catch to handle rejections. Async functions always return a Promise. Keep async logic in dedicated functions rather than mixing sync and async in confusing ways.
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error("User not found");
return response.json();
}
// Usage: loadUser(1) returns a Promise, so await it or .then()
const user = await loadUser(1);
Avoid awaiting inside loops when you can run requests in parallel: use Promise.all instead of a sequential loop so that independent requests complete faster.
Error Handling
Check !response.ok and throw new Error(...) so a single catch block can handle both network and HTTP errors. Surface user-friendly messages and log details for debugging. For forms or retry flows, distinguish transient errors (retry) from permanent ones (show message).
A common pattern is to centralize fetch and error handling in a small helper:
async function api(path, options = {}) {
const response = await fetch(path, options);
if (!response.ok) {
const message = await response.text().catch(() => response.statusText);
throw new Error(message || `HTTP ${response.status}`);
}
return response.json();
}
Then in your UI code you only need one try/catch: network failures and HTTP errors both become a single thrown error. You can then map status codes (e.g. 401 → "Please sign in again", 500 → "Something went wrong, try again later") for user-facing messages.
Loading and Empty States
While a request is in flight, show a loading indicator (skeleton, spinner, or disabled state). When the request fails, show an error message and optionally a retry action. When the list is empty (no error), show an empty state. Avoid flashing "loading" then "empty" if the API returns 200 with an empty array—treat that as a valid empty result.
In React, a simple pattern is a single state variable that can be "idle" | "loading" | "success" | "error". Render loading UI when loading, error UI when error, and empty state when success and the data array is empty. This keeps the UI predictable and avoids the "flash of empty" when the API correctly returns an empty list.
AbortController for Cancellation
Use AbortController to cancel in-flight requests when the user navigates away or changes filters. Pass signal: controller.signal in fetch options and call controller.abort() when needed. The Promise rejects with an AbortError—catch it and ignore if you've intentionally aborted.
const controller = new AbortController();
fetch("/api/search?q=" + query, { signal: controller.signal })
.then((r) => r.json())
.then(setResults)
.catch((err) => {
if (err.name === "AbortError") return; // expected when aborting
setError(err.message);
});
// When the user changes the query or leaves the page:
controller.abort();
In React, create the controller in an effect and abort it in the effect cleanup so that when the component unmounts or the query changes, the previous request is cancelled. That way you avoid race conditions where an older response overwrites a newer one.
Retry and Backoff
For transient failures (network blip, 503), retrying a few times often succeeds. Use a small delay between retries (e.g. 1s, 2s) so you don't hammer the server. Only retry on specific status codes or on network errors; don't retry 4xx (except maybe 429 with Retry-After). After a few failures, show an error and let the user trigger a manual retry.
Summary
Use fetch with async/await, always check response.ok and throw on errors, and handle loading, error, and empty states in the UI. Use AbortController when the user can change inputs or leave the page before the request finishes. Centralize request logic in a small helper so every screen doesn't repeat the same error and parsing code. Consistent fetch usage with these patterns will make your data-loading code predictable and user-friendly.
Related articles
- Frontend FundamentalsDOM and Events
Event loop, event delegation, bubbling and capture, target vs currentTarget, and how to work with the DOM and events in JavaScript.
Read article - Frontend FundamentalsJavaScript Language Basics
Core JavaScript concepts: variables, functions, control flow, objects, arrays, scope, error handling, and modern ES6+ syntax every frontend developer needs.
Read article - Frontend FundamentalsAdvanced JavaScript
Deep dive into the event loop, async patterns, prototypes, closures, functional programming, and JavaScript gotchas every senior developer should know.
Read article - Frontend FundamentalsMastering Frontend Fundamentals: A Mock Interview Guide
Prepare for your frontend developer interview with essential concepts in HTML, CSS, React, and JavaScript, as demonstrated in a mock interview.
Read article