Every app you use pulls data from somewhere, a weather service, a database, a payment processor. In JavaScript, the tool that handles all of that is fetch. This lesson shows you how to use it and what to do when things go wrong.
What fetch() actually does
fetch() is a browser-native function that sends an HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. request and gives you back a PromiseWhat is promise?An object that represents a value you don't have yet but will get in the future, letting your code keep running while it waits.. Think of it like ordering food at a restaurant: you place the order (the request), and the kitchen promises to get back to you. When the food arrives, you still need to unpack it (parse the response).
// The Promise-based way
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));That .then(response => response.json()) step is easy to forget, the first .then gives you the Response wrapper, not the data itself. You have to call .json() to unwrap it, and that call is also asynchronous.
Using async/awaitWhat is async/await?A syntax that lets you write asynchronous code (like fetching data) in a readable, step-by-step style instead of chaining callbacks.
async/await makes fetch calls much easier to read and reason about. Instead of chaining .then() callbacks, you write code that looks synchronous even though it isn't.
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch users:', error);
}
}Notice the two await calls: one for the network request, one for parsing the body. Both are asynchronous operations. Forgetting the second await is one of the most common beginner mistakes, you'll end up with a PromiseWhat is promise?An object that represents a value you don't have yet but will get in the future, letting your code keep running while it waits. object instead of your data.
The Response object
When fetch() resolves, it gives you a Response object with useful properties you can inspect before doing anything with the data.
const response = await fetch('https://api.example.com/users');
// Check if request was successful (status 200-299)
console.log(response.ok); // true or false
console.log(response.status); // 200, 404, 500, etc.
console.log(response.statusText); // "OK", "Not Found", etc.
console.log(response.headers); // Response headers
// Parse response body (pick one - body can only be read once)
const json = await response.json(); // Parse as JSON
const text = await response.text(); // Parse as plain text
const blob = await response.blob(); // Parse as binary (images, files)response.json() and then try response.text(), the second call will throw. Pick the method that matches what the server is returning.| Property | Type | What it tells you |
|---|---|---|
response.ok | boolean | true if status is 200–299 |
response.status | number | HTTP status code (200, 404, 500...) |
response.statusText | string | Human-readable status ("OK", "Not Found") |
response.headers | Headers | Response headers object |
response.body | ReadableStream | Raw body (consumed once) |
response.ok checks, omit try/catch, and assume the happy path always works. A fetch call without if (!response.ok) is incomplete, period. If Copilot hands you a bare fetch → .json() chain, add the error handling yourself before moving on.Building a reusable fetch wrapper
Writing raw fetch() calls everywhere gets repetitive fast. A small wrapper function cleans things up and centralizes your error handling in one place.
const params = new URLSearchParams({
page: '1',
limit: '10',
search: 'javascript'
});
// URLSearchParams builds: ?page=1&limit=10&search=javascript
const response = await fetch(`https://api.example.com/posts?${params}`);
const data = await response.json();async function fetchAPI(endpoint) {
const response = await fetch(`https://api.example.com${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
}
// Clean call sites
const users = await fetchAPI('/users');
const posts = await fetchAPI('/posts');The if (!response.ok) check is critical here. Without it, a 404 or 500 response slips through silently and you'll be parsing an error message as if it were your data.
Quick reference
| Task | Code |
|---|---|
| Basic GET | await fetch(url) |
| Parse JSON response | await response.json() |
| Check success | response.ok (true for 200–299) |
| Add query params | new URLSearchParams({ key: 'value' }) |
| Handle network error | try/catch around the await fetch() call |
| Parse as text | await response.text() |