docs/concepts/http-fetch.mdx
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.
// This is how you fetch data in JavaScript
const 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.
<Info> **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 </Info> <Warning> **Prerequisite:** This guide assumes you understand [Promises](/concepts/promises) and [async/await](/concepts/async-await). Fetch is Promise-based, so you'll need those concepts. If you're not comfortable with Promises yet, read that guide first! </Warning>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.
<Note> **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. </Note> <CardGroup cols={2}> <Card title="HTTP — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/HTTP"> Comprehensive guide to the HTTP protocol </Card> <Card title="An Overview of HTTP — MDN" icon="book" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview"> How HTTP works under the hood </Card> </CardGroup>HTTP follows a simple pattern called request-response. To understand it, imagine you're at a restaurant:
┌─────────────────────────────────────────────────────────────────────────┐
│ THE REQUEST-RESPONSE CYCLE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ YOU (Browser) KITCHEN (Server) │
│ ┌──────────┐ ┌──────────────┐ │
│ │ │ ──── "I'd like pasta" ────► │ │ │
│ │ :) │ (REQUEST) │ [chef] │ │
│ │ │ │ │ │
│ │ │ ◄──── Here you go! ──────── │ │ │
│ │ │ (RESPONSE) │ │ │
│ └──────────┘ └──────────────┘ │
│ │
│ The waiter (HTTP) is the protocol that makes this exchange work! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Sometimes things go wrong:
This request-response cycle is the core of how the web works. The Fetch API is JavaScript's modern way to participate in this cycle programmatically.
Before diving into the Fetch API, let's understand the key concepts of HTTP itself.
Every HTTP interaction follows a simple pattern:
<Steps> <Step title="Client Sends Request"> 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). </Step> <Step title="Server Processes Request"> The server receives the request, does whatever work is needed (database queries, calculations, etc.), and prepares a response. </Step> <Step title="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). </Step> <Step title="Client Handles Response"> Your JavaScript code receives the response and does something with it: display data, show an error, redirect the user, etc. </Step> </Steps>HTTP methods tell the server what action you want to perform. Think of them as verbs:
| Method | Purpose | Restaurant Analogy |
|---|---|---|
| GET | Retrieve data | "Can I see the menu?" |
| POST | Create new data | "I'd like to place an order" |
| PUT | Update/replace data | "Actually, change my order to pizza" |
| PATCH | Partially update data | "Add extra cheese to my order" |
| DELETE | Remove data | "Cancel my order" |
// GET - Retrieve a user
fetch('/api/users/123')
// POST - Create a new user
fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'Alice' })
})
// PUT - Replace a user
fetch('/api/users/123', {
method: 'PUT',
body: JSON.stringify({ name: 'Alice Updated' })
})
// PATCH - Partially update a user
fetch('/api/users/123', {
method: 'PATCH',
body: JSON.stringify({ name: 'New Name' })
})
// DELETE - Remove a user
fetch('/api/users/123', {
method: 'DELETE'
})
Status codes are three-digit numbers that tell you how the request went:
<AccordionGroup> <Accordion title="2xx - Success"> The request was received, understood, and accepted.- **200 OK** — Standard success response
- **201 Created** — New resource was created (common after POST)
- **204 No Content** — Success, but nothing to return (common after DELETE)
```javascript
// 200 OK example
const response = await fetch('/api/users/123')
console.log(response.status) // 200
console.log(response.ok) // true
```
- **301 Moved Permanently** — Resource has a new permanent URL
- **302 Found** — Temporary redirect
- **304 Not Modified** — Use your cached version
Fetch follows redirects automatically by default.
- **400 Bad Request** — Malformed request syntax
- **401 Unauthorized** — Authentication required
- **403 Forbidden** — You don't have permission
- **404 Not Found** — Resource doesn't exist
- **422 Unprocessable Entity** — Validation failed
```javascript
// 404 Not Found example
const response = await fetch('/api/users/999999')
console.log(response.status) // 404
console.log(response.ok) // false
```
- **500 Internal Server Error** — Generic server error
- **502 Bad Gateway** — Server got invalid response from upstream
- **503 Service Unavailable** — Server is overloaded or down for maintenance
```javascript
// 500 error example
const response = await fetch('/api/broken-endpoint')
console.log(response.status) // 500
console.log(response.ok) // false
```
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.
// Fetch in its simplest form
const 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.
// 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)
Unlike XMLHttpRequest, Fetch:
Now that you understand what Fetch is and how it compares to older approaches, let's dive into the details of using it effectively.
Here's what this looks like in code. By default, fetch() uses the GET method, so you don't need to specify it. There are two ways to write this:
Let's break this down step by step:
```javascript
// Step 1: fetch() returns a Promise that resolves to a Response object
const responsePromise = fetch('https://api.example.com/users')
// Step 2: When the response arrives, we get a Response object
responsePromise.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: [...] }
})
```
Let's break this down step by step:
```javascript
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: [...] }
}
```
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:
const response = await fetch('https://api.example.com/users/1')
// Status information
response.status // 200, 404, 500, etc.
response.statusText // "OK", "Not Found", "Internal Server Error"
response.ok // true if status is 200-299
// Response metadata
response.headers // Headers object
response.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 JSON
response.text() // Parse body as plain text
response.blob() // Parse body as binary Blob
response.formData() // Parse body as FormData
response.arrayBuffer() // Parse body as ArrayBuffer
response.bytes() // Parse body as Uint8Array
Most modern APIs return data in JSON format. The Response object has a built-in .json() method that parses the body and returns a JavaScript object:
async function getUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`)
const user = await response.json()
console.log(user.name) // "Alice"
console.log(user.email) // "[email protected]"
return user
}
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:
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
}
// Usage
const 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:
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) |
Accept | Format of data you want back |
Authorization | Authentication credentials |
Cache-Control | Caching instructions |
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:
// Building a URL with query parameters
const 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 fetch
const response = await fetch(url)
You can also use URLSearchParams directly:
const params = new URLSearchParams({
q: 'javascript',
page: '1'
})
// Append to a URL string
const response = await fetch(`/api/search?${params}`)
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:
┌─────────────────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
This code looks fine, but it has a subtle bug. HTTP errors like 404 or 500 slip right through the catch block:
// ❌ WRONG - This misses HTTP errors!
try {
const response = await fetch('/api/users/999')
const data = await response.json()
console.log(data) // Might be an error object!
} catch (error) {
// Only catches NETWORK errors
// A 404 response WON'T end up here!
console.error('Error:', error)
}
The solution is simple: check response.ok before assuming success. This property is true for status codes 200-299 and false for everything else:
// ✓ CORRECT - Check response.ok
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`)
// Check if the HTTP response was successful
if (!response.ok) {
// HTTP error (4xx, 5xx) - throw to catch block
throw new Error(`HTTP error! Status: ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
// Now this catches BOTH network errors AND HTTP errors
console.error('Fetch failed:', error.message)
throw error
}
}
Here's a pattern you can use in real projects: a wrapper function that handles the response.ok check for you:
async function fetchJSON(url, options = {}) {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
})
// Handle HTTP errors
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
error.status = response.status
error.response = response
throw error
}
// Handle empty responses (like 204 No Content)
if (response.status === 204) {
return null
}
return response.json()
}
// Usage
try {
const user = await fetchJSON('/api/users/1')
console.log(user)
} catch (error) {
if (error.status === 404) {
console.log('User not found')
} else if (error.status >= 500) {
console.log('Server error, try again later')
} else {
console.log('Request failed:', error.message)
}
}
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.
async function loadUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error(`Failed to load user: ${response.status}`)
}
const user = await response.json()
return user
} catch (error) {
console.error('Error loading profile:', error)
return null
}
}
// Usage
const user = await loadUserProfile(123)
if (user) {
console.log(`Welcome, ${user.name}!`)
}
Need to fetch multiple resources? Don't await them one by one:
// ❌ SLOW - Sequential requests (one after another)
async function loadDashboardSlow() {
const user = await fetch('/api/user').then(r => r.json())
const posts = await fetch('/api/posts').then(r => r.json())
const notifications = await fetch('/api/notifications').then(r => r.json())
// Total time: user + posts + notifications
return { user, posts, notifications }
}
// ✓ FAST - Parallel requests (all at once)
async function loadDashboardFast() {
const [user, posts, notifications] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/notifications').then(r => r.json())
])
// Total time: max(user, posts, notifications)
return { user, posts, notifications }
}
In real applications, you need to track loading and error states:
async function fetchWithState(url) {
const state = {
data: null,
loading: true,
error: null
}
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
state.data = await response.json()
} catch (error) {
state.error = error.message
} finally {
state.loading = false
}
return state
}
// Usage
const result = await fetchWithState('/api/users')
if (result.loading) {
console.log('Loading...')
} else if (result.error) {
console.log('Error:', result.error)
} else {
console.log('Data:', result.data)
}
The AbortController API lets you cancel in-flight fetch requests. This is useful for:
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.
┌─────────────────────────────────────────────────────────────────────────┐
│ ABORTCONTROLLER FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Create controller 2. Pass signal to fetch │
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
│ │ const controller = │ │ fetch(url, { │ │
│ │ new AbortController│ ───► │ signal: controller.signal │ │
│ └─────────────────────┘ │ }) │ │
│ └─────────────────────────────────┘ │
│ │
│ 3. Call abort() to cancel 4. Fetch rejects with AbortError │
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
│ │ controller.abort() │ ───► │ catch (error) { │ │
│ └─────────────────────┘ │ error.name === 'AbortError' │ │
│ │ } │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
// Create a controller
const controller = new AbortController()
// Pass its signal to fetch
fetch('/api/slow-endpoint', {
signal: controller.signal
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request was cancelled')
} else {
console.error('Request failed:', error)
}
})
// Cancel the request after 5 seconds
setTimeout(() => {
controller.abort()
}, 5000)
Create a reusable timeout wrapper:
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController()
// Set up timeout
const timeoutId = setTimeout(() => {
controller.abort()
}, timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
clearTimeout(timeoutId)
return response
} catch (error) {
clearTimeout(timeoutId)
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeout}ms`)
}
throw error
}
}
// Usage
try {
const response = await fetchWithTimeout('/api/data', {}, 3000)
const data = await response.json()
} catch (error) {
console.error(error.message) // "Request timed out after 3000ms"
}
Cancel previous search when user types:
let currentController = null
async 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 matters
searchInput.addEventListener('input', async (e) => {
const results = await searchUsers(e.target.value)
if (results) {
displayResults(results)
}
})
HTTP is request-response — Client sends a request, server sends a response
HTTP methods are verbs — GET (read), POST (create), PUT (update), DELETE (remove)
Status codes tell you what happened — 2xx (success), 4xx (your fault), 5xx (server's fault)
Fetch returns a Promise — It resolves to a Response object, not directly to data
Response.json() is also a Promise — You need to await it too
Fetch only rejects on network errors — HTTP 404/500 still "succeeds" — check response.ok!
Always check response.ok — This is the most common fetch mistake
Use async/await — It's cleaner than Promise chains
Use Promise.all for parallel requests — Don't await sequentially when you don't have to
AbortController cancels requests — Useful for search inputs and cleanup
</Info>- **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.
```javascript
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
}
```
This is why you need to `await` it:
```javascript
const response = await fetch('/api/data') // Response headers arrived
const data = await response.json() // Body fully downloaded & parsed
```
The same applies to `response.text()`, `response.blob()`, etc.
```javascript
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Alice',
email: '[email protected]'
})
})
```
It's a convenient shorthand for checking if the request succeeded:
```javascript
// 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`
```javascript
// 1. Create controller
const controller = new AbortController()
// 2. Pass its signal to fetch
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('Cancelled!')
}
})
// 3. Call abort() to cancel
controller.abort()
```
Common use cases:
- Timeout implementation
- Cancelling when user navigates away
- Cancelling previous search when user types new input
```javascript
// ✓ Parallel - fast
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
])
// ❌ Sequential - slow (each waits for the previous)
const users = await fetch('/api/users').then(r => r.json())
const posts = await fetch('/api/posts').then(r => r.json())
const comments = await fetch('/api/comments').then(r => r.json())
```
Parallel requests complete in the time of the slowest request, not the sum of all requests.