JAVASCRIPT • WEB PROGRAMMING (BROWSER)

Handling Fetch Responses — Handle JSON responses and HTTP error cases (intro)

Learn to parse server data and gracefully handle errors when your code talks to the web.

Historical Context & Motivation

Modern web applications rarely work in isolation. Every time you scroll through a social media feed, check the weather, or search for a product online, your browser is silently sending requests to a server and processing the responses. In the early days of the web, getting data from a server meant reloading the entire page. Developers needed a better way to fetch data in the background, and that need drove decades of innovation.

The Fetch API is the modern browser tool for making network requests. It replaced the older XMLHttpRequest (XHR) object with a cleaner, promise-based design. Understanding how to handle the responses that come back from fetch() — especially JSON data and HTTP errors — is one of the most essential skills in web programming today.

1999
XMLHttpRequest (XHR) Created
Microsoft introduced XMLHttpRequest in Internet Explorer 5, allowing JavaScript to request data from a server without reloading the page. This was the foundation of AJAX (Asynchronous JavaScript and XML).
2005
AJAX Goes Mainstream
Google Maps and Gmail showcased AJAX to the public, proving that web apps could feel as responsive as desktop software. Developers everywhere began adopting the pattern.
2013
JSON Becomes Dominant
JSON (JavaScript Object Notation) overtook XML as the preferred data format for web APIs, thanks to its lightweight syntax and natural fit with JavaScript.
2015
The Fetch API Arrives
The Fetch API was introduced as a modern, promise-based replacement for XHR. It provided a cleaner interface: fetch(url) returns a Promise that resolves with a Response object.
2020s
Fetch Is Universal
All modern browsers and Node.js 18+ support the Fetch API natively. It is now the standard way to make HTTP requests in JavaScript.

With fetch() now everywhere, a critical question arises: once the server sends back a response, how do you read the data inside it, and what happens when something goes wrong? That is exactly what this lesson addresses.

Core Principles & Definitions

Before diving into code, you need to understand a few foundational ideas. The Fetch API is built on the concept of Promises, which represent values that aren't available yet but will be in the future. When you call fetch(), the browser starts a network request and immediately gives you a Promise. That Promise eventually resolves with a Response object — a container holding everything the server sent back.

1

The Response Object

When fetch() resolves, you get a Response object. It contains properties like status (the HTTP status code), ok (a boolean), and methods to read the body.
2

response.json()

The .json() method reads the response body and parses it as JSON. It returns another Promise that resolves with the actual JavaScript data (objects, arrays, strings, numbers).
3

HTTP Status Codes

Status codes are three-digit numbers the server uses to tell you what happened. Codes 200–299 mean success. Codes 400–499 mean the client made a mistake. Codes 500–599 mean the server had a problem.
4

response.ok

The response.ok property is true when the status code is between 200 and 299. This is your quickest way to check if the request was successful.
5

Fetch Does NOT Reject on HTTP Errors

A critical gotcha: fetch() only rejects its Promise on network failures (no internet, DNS error). A 404 or 500 response still resolves successfully — you must check for errors yourself.
✦ KEY TAKEAWAY
Think of fetch() like ordering a package online. The delivery truck arriving at your door is like the Promise resolving — it means the truck showed up. But you still need to open the box (.json()) to see what's inside, and you need to check the packing slip (response.ok) to make sure they sent the right item rather than an error notice.

Visual Explanation — The Fetch Lifecycle

The following diagram traces the complete lifecycle of a fetch() call, from the initial request through response handling. Pay special attention to the two-step pattern: first you get the Response object, then you read its body.

The diagram shows the two-step pattern: fetch() returns a Response object, then you check response.ok before calling .json(). HTTP errors (like 404) still resolve the Promise — only network failures cause a rejection.

Notice that the green path on the left is the "happy path" — everything works. The red path on the right is your error-handling path, where you detect and manage problems. The warning box at the bottom reminds you that true network failures skip the Response entirely and jump straight to .catch(). This distinction between HTTP errors and network failures is one of the trickiest parts of using fetch().

How It Works — The Two-Step Pattern

Every fetch() call follows the same two-step pattern. In step one, you receive the Response object and check whether the request succeeded. In step two, you parse the body into usable data. Let's look at each step in detail.

Step 1 — Get the Response and Check for Errors

When the server replies, the first .then() callback receives a Response object. This object has several useful properties. The status property holds the numeric HTTP status code (like 200, 404, or 500). The ok property is a boolean shortcut — it's true when status is between 200 and 299, and false otherwise. Since fetch() does not automatically throw an error for 404 or 500 responses, you must check response.ok yourself and throw an error manually if needed.

THE ERROR-CHECK PATTERN
if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); }
response.ok → boolean, true if status 200–299. response.status → the three-digit HTTP status code number.

Step 2 — Parse the Body with .json()

The response body arrives as a stream of raw bytes. You can't use it directly as a JavaScript object yet. Calling response.json() reads the entire stream and parses it as JSON, returning a new Promise that resolves with the parsed data. This is why you need two .then() calls (or two await statements).

THE FULL PATTERN (PROMISE CHAIN)
fetch(url) .then(response => { if (!response.ok) throw new Error(response.status); return response.json(); }) .then(data => { /* use data */ }) .catch(error => { /* handle error */ });
First .then() → checks status and starts parsing. Second .then() → receives the parsed JavaScript data. .catch() → catches both network failures and thrown HTTP errors.
THE FULL PATTERN (ASYNC/AWAIT)
async function getData(url) { try { const response = await fetch(url); if (!response.ok) throw new Error(response.status); const data = await response.json(); return data; } catch (error) { console.error('Fetch failed:', error); } }
await fetch(url) pauses until the Response arrives. await response.json() pauses until the body is fully parsed. The try/catch block handles all errors in one place.
💡 Why Two Awaits?
The first await waits for the server to send the response headers (status code, content type). The second await waits for the entire body to arrive and be parsed. Headers arrive quickly, but the body could be megabytes of data that streams in over time.

HTTP Status Codes — A Detailed Breakdown

Every HTTP response includes a status code — a three-digit number that tells you exactly what happened on the server. These codes are grouped into five categories based on their first digit. As a web developer, you'll encounter certain codes much more frequently than others.

HTTP status codes are grouped by their first digit. Only 2xx codes set response.ok to true. All 4xx and 5xx codes require you to handle the error manually.
Common HTTP status codes you'll encounter when using fetch()
Status CodeMeaningresponse.okCommon Cause
200OK — successtrueNormal successful request
201CreatedtrueSuccessfully created a new resource (POST)
400Bad RequestfalseInvalid data sent to the server
401UnauthorizedfalseMissing or invalid authentication
404Not FoundfalseURL doesn't exist on the server
500Internal Server ErrorfalseBug or crash in the server code

Worked Example — Fetching User Data

Let's walk through a realistic example. Suppose you're building a profile page and need to fetch a user's information from a public API. We'll use the free JSONPlaceholder API, which provides fake data for testing.

Fetching a User Profile from an API
1
Step 1 — Write the fetch callStart by calling fetch() with the API URL. We'll use async/await syntax for clarity. The function must be marked async because we're using await inside it.
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
2
Step 2 — Check if the response is OKBefore trying to read the data, check response.ok. If it's false, throw an error with the status code so it gets caught by the catch block.
if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); }
3
Step 3 — Parse the JSON bodyCall response.json() and await the result. This transforms the raw response body into a JavaScript object you can work with.
const user = await response.json();
4
Step 4 — Use the dataNow you can access properties on the user object. For example, user.name gives the user's name, and user.email gives their email address.
console.log(user.name); // "Leanne Graham" — The parsed JSON object is now a regular JavaScript object.
5
Step 5 — Handle errors in the catch blockWrap everything in a try/catch block. The catch block will handle both HTTP errors (that we threw manually) and network errors (that the browser throws automatically). Display a user-friendly message.
catch (error) { console.error('Failed to load user:', error.message); }
📋 Complete Code
async function getUser() { try { const response = await fetch('https://jsonplaceholder.typicode.com/users/1'); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const user = await response.json(); console.log(user.name); // "Leanne Graham" console.log(user.email); // "Sincere@april.biz" } catch (error) { console.error('Failed to load user:', error.message); } } getUser();

Common Pitfalls & Best Practices

Even experienced developers make mistakes with fetch(). The table below highlights the most common pitfalls alongside the correct approach. Reviewing these will save you hours of debugging.

Five common fetch() mistakes and how to fix them
Common MistakeWhy It's WrongCorrect Approach
Not checking response.okA 404 or 500 response will silently be treated as success, leading to confusing bugs.Always check if (!response.ok) and throw an error.
Forgetting to await the .json() callYou get a pending Promise instead of actual data. Properties will be undefined.Always use await response.json() or chain .then().
Calling .json() on a non-JSON responseIf the server returns HTML or plain text, parsing as JSON will throw a SyntaxError.Check the Content-Type header, or use .text() for non-JSON responses.
No .catch() or try/catchNetwork errors cause an unhandled promise rejection, which can crash your app or silently fail.Always include error handling that shows the user a helpful message.
Trying to read the body twiceResponse bodies can only be consumed once. Calling .json() a second time will throw an error.Store the parsed result in a variable and reuse it.
✦ KEY TAKEAWAY
The single most important rule: always check response.ok. Think of it like checking your GPS after making a turn. The car (your code) keeps driving either way, but without checking the GPS (the status code), you might be heading in completely the wrong direction and not realize it until you're lost.

Connection to Advanced Concepts

The error-checking pattern you've learned here is just the beginning. As you grow as a developer, you'll encounter more sophisticated ways to handle fetch responses. The table below previews some advanced concepts you'll meet next.

From intro-level fetch handling to advanced patterns
What You Know NowWhat Comes Next
Using .json() to parse responsesUsing .text(), .blob(), and .arrayBuffer() for images, files, and binary data
GET requests (reading data)POST, PUT, DELETE requests (creating, updating, and deleting data)
Throwing errors manually on bad statusBuilding a reusable fetchWrapper() utility function that handles errors, retries, and timeouts automatically
Basic try/catch error handlingHandling specific error types differently (network error vs. 401 unauthorized vs. 500 server error) to show the right message
Fetching one resource at a timeUsing Promise.all() to fetch multiple resources in parallel, and AbortController to cancel requests

Real-world applications also need to deal with authentication tokens, rate limiting, caching strategies, and loading states in the UI. But every one of these advanced patterns is built on the same foundation: make a request, check the response, parse the data, and handle errors. Master this pattern now and everything else will click into place.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain why fetch() does not automatically throw an error when the server returns a 404 Not Found response. What would happen if your code does not check response.ok in this situation?
PROBLEM 2 — BASIC CALCULATION
Given the following code, identify what is missing and rewrite it correctly: const response = await fetch('https://api.example.com/data'); const data = await response.json(); console.log(data);
PROBLEM 3 — INTERMEDIATE
Write an async function called getWeather(city) that fetches data from `https://api.weather.example.com/${city}`. The function should return the parsed JSON data on success, or return null and log an error message if anything goes wrong (HTTP error or network failure). Include all necessary error handling.
PROBLEM 4 — APPLIED
You're building a student directory page. Write a function that fetches a list of students from 'https://api.school.example.com/students' and displays each student's name in a <ul> element with the id "student-list". If the request fails, display the message 'Could not load students. Please try again.' inside that same element. Assume the API returns an array of objects like [{name: "Alice"}, {name: "Bob"}].
PROBLEM 5 — CRITICAL THINKING
Consider two scenarios: (A) the server is completely offline and (B) the server returns a 500 Internal Server Error with a JSON body {"error": "Database connection failed"}. In your fetch code, how would each scenario flow differently through the error-handling logic? Could you extract useful information from scenario B that you can't get from scenario A? Explain your reasoning.

Lesson Summary

The Fetch API is the modern way to make network requests in JavaScript. Calling fetch(url) returns a Promise that resolves with a Response object. You must check response.ok to detect HTTP errors (4xx and 5xx status codes) because fetch() does not automatically reject for them. Only true network failures cause a Promise rejection.

To read the data, call response.json(), which returns another Promise resolving with parsed JSON data. This two-step pattern — check the status, then parse the body — is the foundation of all fetch-based code. Always wrap your fetch logic in try/catch (or .catch()) to handle both HTTP errors and network failures gracefully. Master this pattern, and you're ready to build real web applications that communicate with servers.

Varsity Tutors • JavaScript • Handling Fetch Responses — Handle JSON responses and HTTP error cases (intro)