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.

JavaScript emblem with code brackets, braces, and a code file.
Inside this lesson 10 sections 5 questions 8 figures

SECTION 1

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.

SECTION 2

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.

SECTION 3

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.

Fetch API Request Lifecyclefetch(url)Call the functionPromisePending stateNetwork RequestBrowser sends HTTPrequest to serverResponse objectHeaders received (body not yet read)Stage 2: Parse the Body.json()Parse as JSON.text()Parse as text.blob()Parse as binary✅ Use Your Data!JavaScript object ready to use❌ .catch(error)Network failure handlingNote: fetch only rejects on network errors, not on 404/500 status codes
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.

SECTION 4

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))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′))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

asyncfunctiongetData()tryconstresponse=awaitfetch(url);constdata=awaitresponse.json();console.log(data);catch(error)console.error(error);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.

SECTION 5

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 AnatomyResponsePropertiesBody MethodsStatus Codes.statusHTTP code (200, 404, etc.).oktrue if status is 200–299.statusText"OK", "Not Found", etc..headersResponse header map.json()Parse body as JSON.text()Read body as plain text.blob()Read body as binary data.formData()Parse as form data200OK — Success201Created301Moved Permanently400Bad Request404Not Found500Server Error⚠ Important:fetch() does NOT reject on HTTP errors like 404 or 500.You must check response.ok or response.status yourself and throw an error if needed.
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

asyncfunctionfetchData(url)constresponse=awaitfetch(url);if(!response.ok)thrownewError(‘HTTPerror!Status:$response.status‘);constdata=awaitresponse.json();returndata;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.

SECTION 6

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. 1

    Step 1 — Identify the API Endpoint

    We 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. 2

    Step 2 — Write the Fetch Call

    We 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. 3

    Step 3 — Check the Response Status

    Before 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. 4

    Step 4 — Parse the JSON Body

    Since 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. 5

    Step 5 — Display the Data on the Page

    We 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!

SECTION 7

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.

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

Comparison of Fetch API and XMLHttpRequest

✦

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.

SECTION 8

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.

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

From Fetch basics to advanced patterns

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.

SECTION 9

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.

Question 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?
Show answer
The fetch() function returns a Promise, not the actual data. A Promise is a placeholder for a value that will arrive in the future — the network request takes time to travel to the server and back. To get the data, you need to chain .then(response => response.json()) to first resolve the Response object, and then another .then(data => ...) to access the parsed data. Alternatively, you can use await inside an async function to pause execution until the Promise resolves.

Question 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.
Show answer
Here is the solution: async function getPost() { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts/5'); if (!response.ok) { throw new Error('HTTP error: ' + response.status); } const post = await response.json(); return post.title; } catch (error) { console.error('Failed to fetch post:', error); return null; } } This function uses async/await for readability. The if (!response.ok) check catches HTTP errors like 404 that fetch would otherwise treat as successful responses. The catch block handles network failures and any errors thrown inside the try block.

Question 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?
Show answer
Solution: async function createPost() { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'My Post', body: 'Hello world', userId: 1 }) }); if (!response.ok) throw new Error('HTTP error: ' + response.status); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } } You must include the Content-Type: application/json header so the server knows to interpret the request body as JSON rather than plain text or form data. Without this header, the server might reject the request or misinterpret the data. The body must be converted from a JavaScript object to a JSON string using JSON.stringify().

Question 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' }.
Show answer
Solution: async function showWeather() { const tempEl = document.getElementById('temp'); try { const response = await fetch('https://api.example.com/weather?city=Denver'); if (!response.ok) { throw new Error(`Server error: ${response.status} ${response.statusText}`); } const weather = await response.json(); tempEl.textContent = `${weather.temperature}°F — ${weather.conditions}`; } catch (error) { tempEl.textContent = 'Unable to load weather data. Please try again later.'; console.error('Weather fetch failed:', error.message); } } This function follows the robust pattern: it checks response.ok for HTTP errors, uses try/catch for network errors, and always provides user-friendly feedback regardless of the error type. The console.error call preserves the technical error for debugging while the user sees a clean message.

Question 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.
Show answer
There are three key issues. First, the code does not check response.ok, so a 500 Internal Server Error would still attempt to parse the body as JSON, which would likely fail with a confusing parse error rather than a clear HTTP error message. Second, if the server returns non-JSON content (like an HTML error page), .json() would throw a SyntaxError with no helpful context. Third, the catch block only logs to the console — the user sees no feedback. Improved version: async function loadUsers() { const countEl = document.getElementById('count'); try { const response = await fetch('https://api.example.com/users'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const users = await response.json(); countEl.textContent = users.length; } catch (error) { countEl.textContent = 'Error loading users'; console.error('loadUsers failed:', error); } } This version checks the status code first, uses async/await for readability, and shows a user-facing error message. These patterns are essential for building applications that fail gracefully.

SUMMARY

Lesson Summary

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