JAVASCRIPT • WEB PROGRAMMING (BROWSER)

Fetch API — Make HTTP requests using fetch

Learn how modern web pages communicate with servers to load and send data dynamically.

Historical Context & Motivation

Every time you scroll through a social media feed, search for a product online, or check the weather on a website, your browser is silently communicating with a remote server behind the scenes. This communication happens through HTTP requests — messages your browser sends to a server asking for data or submitting information. For years, JavaScript developers relied on an older, clunky tool called XMLHttpRequest (XHR) to make these requests. XHR worked, but its syntax was verbose, confusing, and difficult to manage for anything beyond simple tasks.

The web development community wanted something cleaner and more modern. The result was the Fetch API, a built-in browser function that provides a streamlined, promise-based way to make HTTP requests. Fetch was designed to replace XHR with an interface that is easier to read, easier to chain, and better suited to modern asynchronous JavaScript patterns.

1999
XMLHttpRequest Introduced
Microsoft introduced XMLHttpRequest in Internet Explorer 5, enabling JavaScript to make HTTP requests without reloading the entire page. This was the first step toward dynamic web applications.
2005
The Rise of AJAX
The term AJAX (Asynchronous JavaScript and XML) was coined, popularizing the technique of updating parts of a page without a full reload. Google Maps and Gmail showcased its power.
2014
Fetch API Specification Drafted
The WHATWG (Web Hypertext Application Technology Working Group) began drafting the Fetch standard, aiming to create a modern, promise-based replacement for XHR.
2015
Fetch Lands in Browsers
Chrome, Firefox, and Opera shipped native support for the Fetch API. Developers could now use fetch() without any external libraries.
2017–Present
Universal Adoption
All major browsers, including Edge and Safari, fully support the Fetch API. It is now the standard way to make HTTP requests in client-side JavaScript.

The central question this lesson addresses is straightforward: How does a web page ask a server for data and handle the response using JavaScript's Fetch API? By the end, you will be able to make GET and POST requests, parse JSON responses, and handle errors gracefully.

Core Principles & Definitions

Before diving into code, you need to understand a few foundational ideas that make the Fetch API work. These concepts appear throughout web programming, so mastering them here will serve you well in future projects.

1

HTTP (HyperText Transfer Protocol)

The set of rules that governs how messages are sent between a web browser (the client) and a web server. Common HTTP methods include GET (retrieve data) and POST (send data).
2

Promises

A Promise is a JavaScript object that represents a value that may not be available yet. It will eventually either resolve (succeed) or reject (fail). Fetch returns a Promise, so you handle the result asynchronously.
3

JSON (JavaScript Object Notation)

A lightweight text format for structuring data, using key-value pairs and arrays. Most modern APIs send and receive data in JSON format because it is easy for both humans and machines to read.
4

API (Application Programming Interface)

An API is a defined set of URLs and rules that a server exposes so other programs can request data or perform actions. When you use fetch(), you are typically calling a web API.
5

Asynchronous Execution

Network requests take time. Rather than freezing the entire page while waiting, JavaScript runs these tasks asynchronously — the rest of your code continues executing while the request is in progress.
✦ KEY TAKEAWAY
Think of the Fetch API like ordering food at a restaurant. You place your order (make a request), and instead of standing at the kitchen door waiting, you sit down and continue your conversation (asynchronous execution). The waiter brings you a ticket (a Promise) that guarantees your food will arrive. When the food is ready, it is delivered to your table (the Promise resolves). If the kitchen is out of an ingredient, the waiter tells you (the Promise rejects). The Fetch API works the same way — you send a request and handle the result when it arrives, without blocking everything else.

Visual Explanation — The Fetch Lifecycle

Understanding the lifecycle of a fetch request is crucial. The diagram below illustrates the complete journey from calling fetch() to handling the final data or error. Pay close attention to the two-stage response process — this is one of the most common sources of confusion for beginners.

The Fetch lifecycle has two main stages. In Stage 1, calling fetch() returns a Promise that resolves to a Response object once the server's headers arrive. In Stage 2, you call a method like .json() on the Response to actually read and parse the body — which also returns a Promise.

Notice how the lifecycle involves two separate Promises. The first Promise resolves when the server responds with headers. The second Promise resolves when the response body has been fully read and parsed. This two-stage design lets you inspect response headers (like the status code) before committing to downloading potentially large amounts of data.

How Fetch Works — Syntax & Options

The fetch() function accepts two arguments: a URL (required) and an optional options object that lets you configure the request method, headers, body, and more. When you omit the options object, fetch defaults to a simple GET request.

Basic GET Request Syntax

GET REQUEST PATTERN
fetch(url) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error))
url — the server endpoint you are requesting data from. response — the Response object (headers + status). data — the parsed JSON body. .catch() — handles network failures.

POST Request with Options Object

POST REQUEST PATTERN
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: 'value' }) })
method — the HTTP verb (GET, POST, PUT, DELETE). headers — metadata about the request, like the content type. body — the data you are sending, converted to a JSON string with JSON.stringify().

Async/Await Alternative

ASYNC/AWAIT PATTERN
async function getData() { try { const response = await fetch(url); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } }
async — declares that this function contains asynchronous operations. await — pauses execution until the Promise resolves, making the code read like synchronous steps. try/catch — replaces .catch() for error handling.

Both the .then() chain and the async/await pattern accomplish the same thing. The async/await syntax is generally preferred because it reads top-to-bottom like regular code, which makes it easier to understand and debug. Either approach is valid, and you will encounter both in real projects.

The Response Object — Properties & Methods

When the first Promise resolves, you get a Response object. This object contains important information about what the server sent back — including the status code, response headers, and methods for reading the body. Understanding these properties is essential for writing robust code that handles both success and failure scenarios.

The Response object has three key areas: properties (like .status and .ok), body methods (like .json()), and the status codes that tell you what happened with your request. Notice the warning at the bottom — this is a critical detail.
⚠️ Common Gotcha
One of the most common mistakes beginners make is assuming that a 404 (Not Found) response will trigger the .catch() block. It will not! The fetch() Promise only rejects when the network itself fails — for example, if you are offline or the server is completely unreachable. A 404 response means the server did respond, so the Promise resolves successfully. You need to check response.ok manually to detect these errors.

A robust fetch pattern always checks response.ok before attempting to parse the body. Here is the recommended approach:

ROBUST FETCH PATTERN
async function fetchData(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const data = await response.json(); return data; }
By throwing an error when response.ok is false, you ensure that HTTP errors like 404 and 500 are caught by your try/catch block, just like network failures.

Worked Example — Fetching User Data from an API

Let's walk through a complete, real-world example. We will fetch user data from JSONPlaceholder, a free public API designed for testing and learning. Our goal is to retrieve a user's name and email, then display it on the page.

Fetching and Displaying a User's Name
1
Step 1 — Identify the API EndpointWe need to know the URL that provides user data. JSONPlaceholder offers a users endpoint at https://jsonplaceholder.typicode.com/users/1. The /1 at the end means we are requesting the user with ID 1. This endpoint returns a JSON object with fields like name, email, and phone.
URL: https://jsonplaceholder.typicode.com/users/1
2
Step 2 — Write the Fetch CallWe use an async function with try/catch for clean error handling. Inside the try block, we call fetch() with the URL and await the result. Since we want JSON, we default to a GET request (no options object needed).
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
3
Step 3 — Check the Response StatusBefore parsing the body, we check whether the response was successful using response.ok. If it is false (meaning the server returned a 4xx or 5xx status), we throw a descriptive error that includes the status code.
if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); }
4
Step 4 — Parse the JSON BodySince the API returns JSON, we call response.json() and await the result. This converts the raw JSON text into a JavaScript object we can work with. The parsed data will have properties like user.name and user.email.
const user = await response.json();
5
Step 5 — Display the Data on the PageWe use document.getElementById() to find an HTML element and set its textContent to the user's name. If there is an error at any point, the catch block logs it and shows a friendly message to the user instead.
Complete code: async function showUser() { 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(); document.getElementById('output').textContent = `${user.name} — ${user.email}`; } catch (err) { document.getElementById('output').textContent = 'Failed to load user.'; console.error(err); } }
🧪 Try It Yourself
Open your browser's developer console (press F12, then click the Console tab). Paste this one-liner and press Enter: fetch('https://jsonplaceholder.typicode.com/users/1').then(r => r.json()).then(d => console.log(d)). You should see a JavaScript object with the user's details printed in the console. This is a real HTTP request happening right in your browser!

Strengths & Limitations — Fetch vs. XMLHttpRequest

Knowing when to use Fetch — and understanding its limitations — helps you make informed decisions as a developer. The table below compares the Fetch API with the older XMLHttpRequest approach across several important criteria.

Comparison of Fetch API and XMLHttpRequest
FeatureFetch APIXMLHttpRequest
SyntaxClean, promise-based, chainableVerbose, callback-based, requires multiple event handlers
Promise SupportBuilt-in — works with .then() and async/awaitNo native Promise support; must be wrapped manually
Error HandlingOnly rejects on network errors (must check response.ok)Uses onerror callback; different error handling model
Request CancellationSupported via AbortControllerBuilt-in .abort() method
Upload ProgressNot natively supported (streams API needed)Built-in progress events for uploads
Browser SupportAll modern browsers (post-2017)All browsers, including very old ones
StreamingSupports ReadableStream for large responsesLimited streaming support
✦ KEY TAKEAWAY
The Fetch API is like upgrading from a flip phone to a smartphone. Both can make calls (HTTP requests), but the smartphone (Fetch) has a cleaner interface, better integration with modern apps (Promises), and more built-in capabilities (streaming). The flip phone (XHR) still works and even has a few features the smartphone lacks (like built-in upload progress tracking), but for the vast majority of tasks, the modern option is the better choice.

Connection to Advanced Concepts

Once you are comfortable with basic fetch requests, there is a rich ecosystem of advanced patterns and tools that build on this foundation. Understanding where the Fetch API fits in the bigger picture will help you grow as a developer.

From Fetch basics to advanced patterns
ConceptWhat You Learn Now (Fetch Basics)What Comes Next (Advanced)
Error HandlingCheck response.ok, use try/catchRetry logic, exponential backoff, circuit breaker patterns
Data FormatsParse JSON with .json()GraphQL queries, Protocol Buffers, streaming with ReadableStream
AuthenticationSend basic headersOAuth tokens, JWT (JSON Web Tokens), cookie management
Request ManagementSingle requests with async/awaitAbortController for cancellation, Promise.all for parallel requests, request deduplication
LibrariesNative fetch() in the browserAxios, React Query / TanStack Query, SWR for caching and state management

As you build more complex applications, you will likely use libraries like Axios or React Query that wrap the Fetch API with additional features like automatic retries, caching, and simpler error handling. However, these tools are built on top of the same HTTP fundamentals you have learned here. Mastering fetch() first gives you a solid foundation for understanding what those libraries do under the hood.

🔭 Looking Ahead
If you continue into frameworks like React, Vue, or Angular, you will use fetch (or libraries built on it) constantly. Server-side JavaScript (Node.js) also added native fetch() support in version 18, meaning the skills you learn here transfer directly to back-end development as well.

Practice Problems

Test your understanding of the Fetch API with these five problems, arranged from conceptual to challenging. Try to answer each one before revealing the solution.

PROBLEM 1 — CONCEPTUAL
A student writes fetch('https://api.example.com/data') and expects it to immediately return the data from the server. Why doesn't this work? What does fetch() actually return, and what additional step is needed?
PROBLEM 2 — BASIC
Write a complete async function called getPost that fetches data from https://jsonplaceholder.typicode.com/posts/5, checks if the response is OK, parses it as JSON, and returns the post's title. Include error handling.
PROBLEM 3 — INTERMEDIATE
Write an async function called createPost that sends a POST request to https://jsonplaceholder.typicode.com/posts with a JSON body containing { title: 'My Post', body: 'Hello world', userId: 1 }. Log the server's response. What headers do you need to include, and why?
PROBLEM 4 — APPLIED
You are building a weather dashboard. Write an async function showWeather that fetches data from a weather API endpoint (use 'https://api.example.com/weather?city=Denver' as a placeholder). The function should: (a) handle network errors, (b) handle HTTP errors, (c) display the temperature in an HTML element with id 'temp', and (d) show a user-friendly message if anything goes wrong. Assume the API returns { temperature: 72, conditions: 'Sunny' }.
PROBLEM 5 — CRITICAL THINKING
A developer writes this code, which seems to work but has a subtle problem: fetch('https://api.example.com/users').then(res => res.json()).then(users => { document.getElementById('count').textContent = users.length; }).catch(err => console.log(err)); Identify at least two issues with this code. Rewrite it to be more robust. Consider what happens when the server returns a 500 error, and what happens if the response body is not valid JSON.

Lesson Summary

The Fetch API is JavaScript's modern, built-in way to make HTTP requests from the browser. It replaced the older XMLHttpRequest with a cleaner, promise-based interface. The fetch() function takes a URL and an optional configuration object, and returns a Promise that resolves to a Response object. Reading the response body requires a second asynchronous step — calling .json(), .text(), or another body method.

A critical detail is that fetch only rejects its Promise on network failures, not on HTTP error status codes like 404 or 500. You must always check response.ok to handle these errors properly. Both the .then() chain pattern and the async/await pattern accomplish the same goal; async/await is generally preferred for its readability. With these fundamentals, you are equipped to build dynamic web pages that communicate with servers, load live data, and submit user input — the backbone of every modern web application.

Varsity Tutors • JavaScript • Fetch API — Make HTTP requests using fetch