Learn the JavaScript Fetch API for HTTP requests. Covers GET, POST, response handling, JSON parsing, and AbortController.
How does JavaScript get data from a server? How do you load user profiles, submit forms, or fetch the latest posts from an API? The answer is the Fetch API, JavaScript’s modern way to make network requests. According to the HTTP Archive’s 2023 Web Almanac, the median web page makes over 70 HTTP requests, making efficient network handling essential for performance.
Copy
Ask AI
// This is how you fetch data in JavaScriptconst response = await fetch('https://api.example.com/users/1')const user = await response.json()console.log(user.name) // "Alice"
But to understand Fetch, you need to understand what’s happening underneath: HTTP.
What you’ll learn in this guide:
How HTTP requests and responses work
The five main HTTP methods (GET, POST, PUT, PATCH, DELETE)
How to use the Fetch API to make requests
Reading and parsing JSON responses
The critical difference between network errors and HTTP errors
Modern patterns with async/await
How to cancel requests with AbortController
Prerequisite: This guide assumes you understand Promises and async/await. Fetch is Promise-based, so you’ll need those concepts. If you’re not comfortable with Promises yet, read that guide first!
HTTP (Hypertext Transfer Protocol) is the foundation of data communication on the web. Originally defined in RFC 2616 and updated through RFC 7230-7235, it defines how messages are formatted and transmitted between clients (like web browsers) and servers. Every time you load a webpage, submit a form, or fetch data with JavaScript, HTTP is the protocol making that exchange possible.
HTTP is not JavaScript. HTTP is a language-agnostic protocol. Python, Ruby, Go, Java, and every other language uses it too. We cover HTTP basics in this guide because understanding the protocol helps with using the Fetch API effectively. If you want to dive deeper into HTTP itself, check out the MDN resources below.
Your browser (the client) sends an HTTP request to a server. The request includes what you want (the URL), how you want it (the method), and any additional info (headers, body).
2
Server Processes Request
The server receives the request, does whatever work is needed (database queries, calculations, etc.), and prepares a response.
3
Server Sends Response
The server sends back an HTTP response containing a status code (success/failure), headers (metadata), and usually a body (the actual data).
4
Client Handles Response
Your JavaScript code receives the response and does something with it: display data, show an error, redirect the user, etc.
The Fetch API is JavaScript’s modern interface for making HTTP requests. It provides a cleaner, Promise-based alternative to the older XMLHttpRequest, letting you send requests to servers and handle responses with simple, readable code. Every modern browser supports Fetch natively.
Copy
Ask AI
// Fetch in its simplest formconst response = await fetch('https://api.example.com/data')const data = await response.json()console.log(data)
Before Fetch existed, developers used XMLHttpRequest (XHR), a verbose, callback-based API that powered “AJAX” requests. Libraries like jQuery became popular partly because they simplified this painful process. jQuery was revolutionary for JavaScript. For many years it was the go-to library that made DOM manipulation, animations, and AJAX requests much easier. It changed how developers wrote JavaScript and shaped the modern web.
Copy
Ask AI
// The old way: XMLHttpRequest (verbose and callback-based)const xhr = new XMLHttpRequest()xhr.open('GET', 'https://api.example.com/data')xhr.onload = function() { if (xhr.status === 200) { const data = JSON.parse(xhr.responseText) console.log(data) }}xhr.onerror = function() { console.error('Request failed')}xhr.send()// The modern way: Fetch (clean and Promise-based)const response = await fetch('https://api.example.com/data')const data = await response.json()console.log(data)
// Step 1: fetch() returns a Promise that resolves to a Response objectconst responsePromise = fetch('https://api.example.com/users')// Step 2: When the response arrives, we get a Response objectresponsePromise.then(response => { console.log(response.status) // 200 console.log(response.ok) // true console.log(response.headers) // Headers object // Step 3: The body is a stream, we need to parse it // .json() returns ANOTHER Promise return response.json()}).then(data => { // Step 4: Now we have the actual data console.log(data) // { users: [...] }})
Copy
Ask AI
// Using async/await - cleaner syntaxasync function getUsers() { try { const response = await fetch('https://api.example.com/users') const data = await response.json() console.log(data) } catch (error) { console.error('Error:', error) }}
Let’s break this down step by step:
Copy
Ask AI
async function getUsers() { // Step 1: await pauses until the Response arrives const response = await fetch('https://api.example.com/users') console.log(response.status) // 200 console.log(response.ok) // true console.log(response.headers) // Headers object // Step 2: await again to read and parse the body const data = await response.json() // Step 3: Now we have the actual data console.log(data) // { users: [...] }}
Which should you use?async/await is generally preferred for its cleaner, more readable syntax. Use .then() chains when you need to integrate with older codebases or when you specifically want to avoid async functions.
When fetch() resolves, you get a Response object. This object contains everything about the server’s reply: status codes, headers, and methods to read the body:
Copy
Ask AI
const response = await fetch('https://api.example.com/users/1')// Status informationresponse.status // 200, 404, 500, etc.response.statusText // "OK", "Not Found", "Internal Server Error"response.ok // true if status is 200-299// Response metadataresponse.headers // Headers objectresponse.url // Final URL (after redirects)response.type // "basic", "cors", etc.response.redirected // true if response came from a redirect// Body methods (each returns a Promise)response.json() // Parse body as JSONresponse.text() // Parse body as plain textresponse.blob() // Parse body as binary Blobresponse.formData() // Parse body as FormDataresponse.arrayBuffer() // Parse body as ArrayBufferresponse.bytes() // Parse body as Uint8Array
Important: The body can only be read once! If you call response.json(), you can’t call response.text() afterward. If you need to read it multiple times, clone the response first with response.clone().
So far we’ve only retrieved data. But what about sending data, like creating a user account or submitting a form?That’s where POST comes in. It’s the HTTP method that tells the server “I’m sending you data to create something new.” To make a POST request, you need to specify the method, set a Content-Type header, and include your data in the body:
Copy
Ask AI
async function createUser(userData) { const response = await fetch('https://api.example.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }) const newUser = await response.json() return newUser}// Usageconst user = await createUser({ name: 'Bob', email: '[email protected]'})console.log(user.id) // New user's ID from server
HTTP headers are metadata you send with your request: things like authentication tokens, content types, and caching instructions. You pass them as an object in the headers option:
Copy
Ask AI
const response = await fetch('https://api.example.com/data', { method: 'GET', headers: { // Tell server what format we want 'Accept': 'application/json', // Authentication token 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...', // Custom header 'X-Custom-Header': 'custom-value' }})
Common headers you’ll use:
Header
Purpose
Content-Type
Format of data you’re sending (e.g., application/json)
When fetching data, you often need to include query parameters (e.g., /api/search?q=javascript&page=1). Use the URL and URLSearchParams APIs to build URLs safely:
Copy
Ask AI
// Building a URL with query parametersconst url = new URL('https://api.example.com/search')url.searchParams.set('q', 'javascript')url.searchParams.set('page', '1')url.searchParams.set('limit', '10')console.log(url.toString())// "https://api.example.com/search?q=javascript&page=1&limit=10"// Use with fetchconst response = await fetch(url)
You can also use URLSearchParams directly:
Copy
Ask AI
const params = new URLSearchParams({ q: 'javascript', page: '1'})// Append to a URL stringconst response = await fetch(`/api/search?${params}`)
Why use URL/URLSearchParams instead of string concatenation? These APIs automatically handle URL encoding for special characters. If a user searches for “C++ tutorial”, it becomes q=C%2B%2B+tutorial. Something you’d have to handle manually with string concatenation.
Here’s a mistake almost every developer makes when learning fetch:
“I wrapped my fetch in try/catch, so I’m handling all errors… right?”
Wrong. The problem? fetch() only throws an error when the network fails, not when the server returns a 404 or 500. A “Page Not Found” response is still a successful network request from fetch’s perspective!
When working with fetch(), there are two completely different types of failures:
Copy
Ask AI
┌─────────────────────────────────────────────────────────────────────────┐│ TWO TYPES OF FAILURES │├─────────────────────────────────────────────────────────────────────────┤│ ││ 1. NETWORK ERRORS 2. HTTP ERROR RESPONSES ││ ──────────────────── ─────────────────────── ││ ││ • Server unreachable • Server responded with error ││ • DNS lookup failed • 404 Not Found ││ • No internet connection • 500 Internal Server Error ││ • Request timed out • 401 Unauthorized ││ • CORS blocked • 403 Forbidden ││ ││ Promise REJECTS ❌ Promise RESOLVES ✓ ││ Goes to .catch() response.ok is false ││ │└─────────────────────────────────────────────────────────────────────────┘
The Trap:fetch() only rejects its Promise for network errors. An HTTP 404 or 500 response is still a “successful” fetch. The network request completed! You must check response.ok to detect HTTP errors.
The examples above use .then() chains, but modern JavaScript has a cleaner syntax: async/await. If you’re not familiar with it, check out our async/await concept first. It’ll make your fetch code much easier to read.
The AbortController API lets you cancel in-flight fetch requests. This is useful for:
Timeouts — Cancel requests that take too long
User navigation — Cancel pending requests when user leaves a page
Search inputs — Cancel the previous search when user types new characters
Component cleanup — Cancel requests when a React/Vue component unmounts
Without AbortController, abandoned requests continue running in the background, wasting bandwidth and potentially causing bugs when their responses arrive after you no longer need them.
let currentController = nullasync function searchUsers(query) { // Cancel any in-flight request if (currentController) { currentController.abort() } // Create new controller for this request currentController = new AbortController() try { const response = await fetch(`/api/search?q=${query}`, { signal: currentController.signal }) if (!response.ok) throw new Error('Search failed') return await response.json() } catch (error) { if (error.name === 'AbortError') { // Ignore - we cancelled this on purpose return null } throw error }}// As user types, only the last request matterssearchInput.addEventListener('input', async (e) => { const results = await searchUsers(e.target.value) if (results) { displayResults(results) }})
This example uses browser DOM APIs (addEventListener, searchInput). In Node.js or server-side contexts, you would trigger the search function differently, but the AbortController pattern remains the same.
Question 1: What's the difference between a network error and an HTTP error in fetch?
Answer:
Network errors occur when the request can’t be completed at all — server unreachable, DNS failure, no internet, CORS blocked, etc. These cause the fetch Promise to reject.
HTTP errors occur when the server responds with an error status code (4xx, 5xx). The request completed successfully (the network worked), so the Promise resolves. You must check response.ok to detect these.
Copy
Ask AI
try { const response = await fetch('/api/data') // This line runs even for 404, 500, etc.! if (!response.ok) { throw new Error(`HTTP ${response.status}`) } const data = await response.json()} catch (error) { // Now catches both types}
Question 2: Why does response.json() return a Promise?
Answer: The response body is a readable stream that might still be downloading when fetch() resolves. The response.json() method reads the entire stream and parses it as JSON, which is an asynchronous operation.This is why you need to await it:
Copy
Ask AI
const response = await fetch('/api/data') // Response headers arrivedconst data = await response.json() // Body fully downloaded & parsed
The same applies to response.text(), response.blob(), etc.
Question 3: How do you send JSON data in a POST request?
Answer:response.ok is a boolean that’s true if the HTTP status code is in the 200-299 range (success), and false otherwise.It’s a convenient shorthand for checking if the request succeeded:
Copy
Ask AI
// These are equivalent:if (response.ok) { ... }if (response.status >= 200 && response.status < 300) { ... }
Common values:
200, 201, 204 → ok is true
400, 401, 404, 500 → ok is false
Question 5: How do you cancel a fetch request?
Answer: Use an AbortController:
Copy
Ask AI
// 1. Create controllerconst controller = new AbortController()// 2. Pass its signal to fetchfetch('/api/data', { signal: controller.signal }) .then(r => r.json()) .catch(error => { if (error.name === 'AbortError') { console.log('Cancelled!') } })// 3. Call abort() to cancelcontroller.abort()
Common use cases:
Timeout implementation
Cancelling when user navigates away
Cancelling previous search when user types new input
Question 6: How do you make multiple fetch requests in parallel?
Answer: Use Promise.all() to run requests concurrently:
The Fetch API is JavaScript’s modern interface for making HTTP requests. It returns Promises, supports streaming responses, and works with the Request and Response objects defined in the WHATWG Fetch Living Standard. It replaced the older XMLHttpRequest API and is now supported in all modern browsers and Node.js 18+.
What is the difference between fetch and XMLHttpRequest?
Fetch uses Promises instead of callbacks, has a cleaner API, supports streaming, and integrates with Service Workers. XMLHttpRequest (XHR) is callback-based and older but supports progress events natively. According to the HTTP Archive’s 2023 Web Almanac, Fetch usage has surpassed XHR in modern web applications, though XHR remains in legacy codebases.
Why does fetch not throw an error on HTTP 404 or 500 responses?
Fetch only rejects its Promise on network failures (no internet, DNS errors, CORS blocked). An HTTP 404 or 500 is still a successful network response — the server replied. You must check response.ok or response.status manually to detect HTTP errors. This is the most common Fetch gotcha for beginners.
How do you cancel a fetch request in JavaScript?
Use the AbortController API. Create an AbortController, pass its signal to fetch(), and call controller.abort() when you need to cancel. This throws an AbortError that you can catch. AbortController was added to the DOM specification and is supported in all modern browsers.
What is the difference between GET and POST requests?
GET requests retrieve data and should have no side effects — they are safe and idempotent. POST requests send data to create or modify resources. GET parameters go in the URL query string, while POST data goes in the request body. As defined in the HTTP/1.1 specification (RFC 7231), GET responses can be cached by browsers, but POST responses are not cached by default.