Mastering the Fetch API – Modern HTTP Requests in JavaScript
The Fetch API is the modern replacement for XMLHttpRequest and the foundation of how JavaScript applications communicate with servers. It is available in every modern browser and in Node.js since version 18. This guide covers everything from basic usage to patterns you will use in production.
The Basics
A fetch request returns a Promise that resolves to a Response object:
1 | |
The .json() method reads the response body and parses it as JSON. It returns a Promise, which is why there are two .then() calls. Other body-reading methods include .text(), .blob(), .formData(), and .arrayBuffer().
The async/await version is cleaner and is preferred in modern code:
1 | |
The critical detail that trips up beginners is that fetch() only rejects the Promise on network errors — the request could not reach the server at all. HTTP error responses like 404 or 500 do not cause a rejection. The fetch succeeds, and you get a Response object with ok: false. You must check response.ok or response.status yourself. This is by design, not a bug — the server successfully responded, just with an error status.
Sending Data
Beyond GET requests, fetch accepts a second argument: an options object. The most common options are method, headers, and body:
1 | |
For file uploads, use FormData instead of JSON. Do not set the Content-Type header when using FormData — the browser sets it automatically with the correct multipart boundary:
1 | |
Aborting Requests
In single-page applications, a user might navigate away from a page before a fetch completes. Continuing to process the response wastes resources and can cause state updates on unmounted components. AbortController solves this:
1 | |
The AbortError is thrown when the request is cancelled. It is important to distinguish it from actual network errors so you do not log it as a real failure.
A common pattern in React: abort a fetch when a component unmounts:
1 | |
The cleanup function returned from useEffect calls controller.abort(), which aborts any in-flight request when the component unmounts or the dependency changes. This prevents the classic “setState on unmounted component” warning.
Custom Request Objects
For reusable request configurations, construct a Request object:
1 | |
Request objects are useful for creating a base configuration and overriding specific properties for different calls. They can also be passed between functions as a single unit rather than spreading multiple configuration arguments.
Handling Different Response Types
Not every endpoint returns JSON. Handle each type appropriately:
1 | |
For streaming, the response body is a ReadableStream. This is useful for processing large datasets in chunks rather than loading everything into memory. Server-Sent Events and streaming API responses are increasingly common, and the Fetch API provides the low-level primitives to handle them.
Error Handling Strategy
A production-grade fetch wrapper should handle network errors, HTTP errors, and timeouts consistently:
1 | |
This wrapper provides automatic timeout handling, consistent error formatting, and JSON parsing. It can be extended with retry logic, request deduplication, and caching. The key principle is that every part of your application that makes HTTP requests should use the same wrapper, so error handling and request configuration are consistent.
Common Patterns
For parallel independent requests, use Promise.all:
1 | |
For dependent requests where one call depends on the result of another, do not use Promise.all. Chain them sequentially:
1 | |
For uploading with progress tracking, fetch does not directly support upload progress events. Use XMLHttpRequest for that specific use case, or use the fetch ReadableStream for download progress tracking.
The Fetch API is simple at the surface and capable of handling most HTTP communication patterns in modern web applications. The combination of async/await, AbortController, and a consistent wrapper provides everything you need for clean, maintainable API communication.