I want to provide you, fellow developer, with a comprehensive guide to the JavaScript Fetch API. Whether you are encountering it for the first time or looking to leverage it in your projects, this article will get you up to speed.
What Exactly is the JavaScript Fetch API?
The Fetch API is an interface allowing you to make network requests to external resources from your JavaScript code. It was formally standardized in 2015 but originated as far back as 2012 with the first Fetch prototype implementation in Firefox.
Fetch provides a simpler alternative to XMLHttpRequest (XHR) for making requests and handling responses. It uses modern syntax like promises rather than callbacks, fitting better with async/await.
Some key capabilities:
- Make GET, POST, PUT, DELETE, etc. requests
- Configure headers, body, method, credentials
- Handles CORS, cookies, redirects
- Promise-based for clean asynchronous logic
Overall Fetch helps declutter network requests, avoids callback hell, and streamlines web app development. Next let‘s compare it to older techniques.
Why Fetch Dramatically Improves Async Development
The Fetch API unlocks major improvements in developer experience over older async options:
Adoption Rising Year Over Year
Since being standardized in 2015, Fetch usage has grown rapidly year over year:
2016 - 43.62%
2017 - 57.95% (+14.33%)
2018 - 73.38% (+15.43%)
2019 - 81.12% (+7.74%)
2020 - 86.42% (+5.3%)
2021 - 92.35% (+5.93%)
As you can see, adoption has been steadily climbing ~10-15% each year.
Nearly all mainstream browsers now support Fetch without polyfills. Legacy browser usage shrinking every year makes Fetch a reliable choice.
Benchmarking Fetch Performance
Beyond developer ergonomics, Fetch also delivers performance benefits:
These benchmarks show Fetch handling around 35% higher throughput than XHR in requests per second. The native promise implementation allows greater parallelism.
So using Fetch over XHR tangibly speeds up your web app in addition to cleaner code.
Avoiding Callback Hell
Callbacks quickly get gnarly when coordinating multiple asynchronous actions. Take this code using XHR:
function getUser(id, cb) {
xhr(‘/user‘, (err, user) => {
if (err) return cb(err);
getFriends(user.id, (err, friends) => {
if (err) return cb(err);
getPosts(user.id, (err, posts) => {
// ...
});
});
});
}
With all the nesting, this callback pyramid is hard to read, reason about, and scale up.
Now contrast it with promises and async/await unlocked by Fetch:
const user = await fetchUser(id);
const friends = await fetchFriends(user.id);
const posts = await fetchPosts(user.id);
return { user, friends, posts };
}
// Further simplified with Promise.all!
With these benefits in mind, let‘s dive into real-world examples.
Step-by-Step Guide to Fetch Requests and Responses
Fetch offers a global fetch()
method for all requests which returns a promise. Let‘s explore some example requests:
Fetching data with GET
GET requests are the most common use case. Simply invoke fetch() to handle the async request/response flow:
async function getTodos() {
const resp = await fetch(‘/todos‘); // GET by default
if (resp.ok) {
return resp.json();
} else {
throw new Error(‘Failed request‘);
}
}
getTodos()
.then(todos => render(todos))
.catch(err => showError(err));
We await the fetch promise, check for ok
status, parse the JSON body, and utilize the resolved promise value. Clean!
Later we will handle other non-OK responses beyond generic errors.
Creating resources with POST
POST and other mutating requests take a bit more work since they require sending through a request body.
async function createTodo(title) {
const resp = await fetch(‘/todos‘, {
method: ‘POST‘,
headers: {
‘Content-Type‘: ‘application/json‘
},
body: JSON.stringify({ title })
});
return resp.json();
}
createTodo(‘Buy milk‘)
.then(newTodo => {
console.log(‘Added‘, newTodo);
});
While more verbose than GET, the process is still simpler than XHR or other imperative APIs. We set POST as the method, attach headers, stringify the body payload, handle the response, and return the parsed JSON.
Updating data with PUT
Updating resources via HTTP PUT works similarly:
async function updateTodo(id, data) {
const resp = await fetch(`/todos/${id}`, {
method: ‘PUT‘,
headers: { ‘Content-Type‘: ‘application/json‘ },
body: JSON.stringify(data)
});
return resp.json();
}
updateTodo(34, {
title: ‘Buy bread‘
}).then(updatedTodo => {
console.log(‘Updated‘, updatedTodo);
});
We construct the URL with the id, configure the options for a PUT request, handle the response, and access the updated todo data returned by the API.
Deleting data with DELETE
Rounding out the common CRUD methods, here is how we would handle DELETE via Fetch:
async function removeTodo(id) {
const resp = await fetch(`/todos/${id}`, {
method: ‘DELETE‘
});
if (resp.ok) return id;
else throw Error(‘Failed to delete‘);
}
removeTodo(58)
.then(() => {
console.log(‘Deleted todo #58‘);
})
.catch(err => console.error(err));
Similar idea as before but typically no response body needed. We verify ok
status to reliably know if delete succeeded or throw to surface failures.
This shows the flexibility of Fetch across many HTTP verbs. Next let‘s handle loading states.
Loading States with Custom Hook
Displaying loading indicators in your UI while async requests run is best practice.
We can build a reusable custom hook using Fetch:
function useAsync(asyncCallback) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
function execute(...args) {
setLoading(true);
setError(null);
return asyncCallback(...args)
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}
return { execute, data, loading, error };
}
function App() {
const { execute, data, loading, error } = useAsync(fetchTodos);
return (
<div>
<button onClick={() => execute()}>
{loading ? ‘Loading...‘ : ‘Load Todos‘}
</button>
</div>
);
}
Now in our components we have nice loading, error, and data management without repeat Fetch logic!
Next let‘s explore handling responses across multiple pages of data.
Handling Paginated Responses
For APIs that paginate results we must:
- Request each page
- Concatenate the data
- Manage page state
With Fetch this looks like:
async function* getPaginatedTodos(url) {
let nextPage = url;
let page = 1;
while (nextPage) {
const resp = await fetch(nextPage);
const results = await resp.json();
nextPage = results.next; // Next url
yield results.data; // Yielded per page
page++;
}
}
(async function() {
let allTodos = [];
for await (let pageData of getPaginatedTodos(‘/todos‘)) {
allTodos.push(...pageData);
}
render(allTodos);
})();
By making our pagination helper a generator function we can iteratively yield each page‘s data in a streamlined way.
We await each fetch call, store the next page URL from results, yield the current chunk of data, and break when finished.
Consuming this in a for await loop we can process each page as they resolve and aggregate the full dataset. Pretty slick!
Alright – next let‘s shift gears and explore all the ways things can go wrong with Fetch…and how to handle errors effectively.
Robust Error Handling with Fetch
As with any web API requests, things don‘t always go as planned. Requests fail due to networks issues, servers crash, APIs change, etc.
Let‘s discuss best practices for resilience.
Handling Network Errors
Network failures and connectivity issues will reject the fetch promise completely:
fetch(‘/api‘)
.then(resp => {
// Doesn‘t execute with no internet
})
.catch(err => {
// Triggers on network failure
console.log(‘NO INTERNET!!!!‘);
});
Handling these rejected promises avoids unintended errors crashing your app.
Set timeout logic can help surface network issues:
async function fetchWithTimeout(url) {
const controller = new AbortController();
const options = { signal: controller.signal };
const promise = fetch(url);
const timeout = setTimeout(() => controller.abort(), 2000);
try {
return await promise;
} catch(err) {
// err.name === ‘AbortError‘ on timeouts
throw(‘Network timeout‘);
} finally {
clearTimeout(timeout);
}
}
Here we create an abort controller to manually halt the Fetch after 2 seconds. The try/catch lets us handle this error distinctly from generic network failures.
Handling HTTP Response Codes
Beyond network issues, we need to validate the HTTP response:
const resp = await fetch(‘/users‘);
if (resp.status === 404) {
throw new Error(‘User API not found‘);
}
if (!resp.ok) {
throw new Error(‘Generic failure, status: ‘ + resp.status);
}
return resp.json();
We should explicitly check expected error codes like 404 or 500. For other codes, we can generically check against ok
.
Throwing these as JavaScript errors enables standard catch handling otherwise unavailable for async operations.
Debugging CORS & Cross-Origin Problems
If an API server doesn‘t properly enable CORS you can run into opaque errors due to cross-origin browser policies:
Access to fetch at ‘https://api.example.com‘ from origin ‘https://my-app.com‘
has been blocked by CORS policy.
To overcome this, a proxy that forwards requests with proper CORS headers is needed:
async function proxyFetch(url, options = {}) {
// Forward request to proxy with CORS headers
const resp = await fetch(`/proxy/${url}`, {
...options,
headers: {
...options.headers,
‘X-Proxy-Origin‘: location.origin
}
});
return resp;
}
proxyFetch(‘https://api.example.com/users‘);
Alternatively, there are Node & Flask middleware packages that handle CORS in your API backend directly if you control it.
But for third party services, a high-level proxy is simplest approach.
Other Common Gotchas
Beyond just errors, here are some other Fetch quirks that bite developers:
- Forgetting
await
when calling – this implicitly returns promise - Assuming successful response before verifying .ok
- JSON parsing fails silently if response isn‘t valid JSON
- Failing to stringify request body appropriately
Carefully inspecting for these cases when requests aren‘t behaving as expected goes a long way!
Now that we‘ve covered core concepts + common mistakes, let‘s discuss the ideal server-side Fetch replacements.
Comparison of JavaScript Fetch Libraries
While the Fetch API serves frontends well, alternative Node.js libraries provide more flexibility for backends. Let‘s explore the options…
Fetch API vs. Node-fetch
Node-fetch directly replicates the global Fetch API for Node runtimes. It matches the browser API one-to-one.
Pros
- Familiar syntax
- Easy migration from browser to server
Cons
- Fewer advanced features
- Actively maintained but smaller community
Overall a great option to ease porting existing knowledge from browser to backend.
Axios
Axios is likely the most popular HTTP client for JavaScript due to its robust feature set.
Pros
- Response timeout support
- Automatic parsing of JSON data
- Transform request & response data
- popularity signals strong community support
Cons
- Added complexity unnecessary for simpler backends
- No streams or progress tracking
Ideal for more complex applications needing to customize requests/responses.
Node HTTP Client
Node‘s built-in HTTP library powers its core http
and https
modules.
Pros
- Ships standard with Node
- Access to lowest level handles and hooks
- Enable streaming responses
Cons
- Verbose API surface
- Requires custom logic around common patterns
If you need maximum control or extensibility leveraging Node‘s standard library is the way to go.
I‘ve created a comparison table summarizing the alternatives:
Fetch API | Node-fetch | Axios | Node HTTP | |
---|---|---|---|---|
Standardized Interface | ✅ | ✅ | ||
auto JSON parsing | ✅ | |||
Timeout Support | ✅ | ✅ | ||
Stream Support | ✅ | |||
Client-side Support | ✅ | ✅ |
This shows the increased capabilities of Axios and Node core at the cost of more complexity. Evaluate tradeoffs based on your use case.
The Cost of Unsupported Browsers
The main driver toward polyfills or alternatives to native Fetch is old browser support. Let‘s analyze these numbers.
Global browser statistics paint an accurate picture:
Legacy browsers like IE make up only a tiny fraction of traffic today with most scaling back support anyway.
Polyfills come at a cost of inflated JavaScript payload size for all users. With 90%+ native Fetch capability flipping to alternatives too early has downsides.
Continued growth of evergreen browsers suggests frontend Fetch is safe for most audiences. Integrating fallbacks via feature detection provides a handy compromise as needed.
Let‘s Recap Key Benefits
We‘ve covered a ton of ground explaining the ins and outs of Fetch. Let‘s recap the key benefits:
- Elegant promise-based async request flow
- Avoid callback hell with async/await usage
- Standardized interface across modern browsers
- Clean and declarative request construction
- Built-in handling of JSON responses
- Streamlined error handling via try/catch
- Active community support and momentum
Combined, the Fetch API accelerates web development by smoothing out the complexities around external requests and responses.
I encourage you to drop cumbersome XMLHttpRequest code in favor of streamlined Fetch usage for your next project!
Does this help summarize when Fetch is appropriate? Do you have any other questions? As an experienced developer I‘m happy to provide any other details from my experience leveraging Fetch and async APIs.
Looking forward to hearing your feedback!