Editor’s note: This article was last reviewed and updated by Elijah Asaolu in January 2025 to cover common errors with Fetch not being defined or failing, and to provide guidance on making GET
and POST
requests.
The stabilization of the Fetch API in Node.js has been one of the most anticipated upgrades in recent years, as it provides a standardized and modern approach to performing HTTP requests in both the browser and server environments. To better understand why this is such a big deal, let’s explore the history of HTTP requests, how Fetch came to be, and what its stabilization means for Node developers.
In the early days of the web, it was difficult to perform asynchronous requests across websites; developers had to use clumsy approaches to interact across multiple networks.
Internet Explorer 5 changed this in 1998 with the introduction of the XMLHttpRequest
API. Initially, XMLHttpRequest
was designed to fetch XML data via HTTP, hence the name. Sometime after it was released, however, support for other data formats — primarily JSON, HTML, and plaintext — was added.
The XMLHttpRequest
API worked like a charm back then, but as the web grew, it became so difficult to work with that JavaScript frameworks (notably jQuery) had to abstract it to make implementation easier and success/error handling smoother.
In 2015, the Fetch API was launched as a modern successor to XMLHttpRequest
, and it has subsequently become the de facto standard for making asynchronous calls in web applications. One significant advantage Fetch has over XMLHttpRequest
is that it leverages promises, allowing for a simpler and cleaner API while avoiding callback hell.
Though the Fetch API has been around for a while now, it wasn’t included in the Node.js core because of some limitations. In a question answered by one of Node’s core contributors, it was noted that the browser’s Fetch API implementation is dependent on a browser-based Web Streams API and the AbortController
interface (for aborting fetch requests), which wasn’t available in Node until recently. As such, it was difficult to choose the best approach to include it in the Node core.
Long before the addition of the Fetch API, the request module was the most popular method for making HTTP requests in Node. But the JavaScript ecosystem at large quickly evolved, and newly introduced patterns made the request module obsolete. A crucial example here is async/await
; there was no provision for this in the request API, and the project was later deprecated due to these limitations.
In 2018, Undici was introduced as a newer and faster HTTP/1.1 client for Node.js, with support for pipelining and pooling, among other features. The Node core team worked hard on Undici, fixing performance issues, guaranteeing stability, and aligning the library with the Node project’s goals.
Undici served as the foundation for Node.js’ native fetch()
implementation, which provided a high-performance, standards-compliant solution for performing HTTP requests. With the integration of Undici into the Node.js core, developers obtained access to a strong and fast HTTP client, laying the foundation for the inclusion of the Fetch API in Node.js v18 and finally stable in v21.
As previously mentioned, Fetch was added to the Node.js core in v18. However, until v21, it was mostly experimental, with unavoidable bugs and instabilities. The stable release in v21 is a big milestone for developers, as it means that it has been tested thoroughly, so you can trust it to work in different scenarios without surprises.
The Fetch API is provided as a high-level function, which means you don’t have to do any import/require
before using it in your Node.js applications.
In its most basic version, it takes a URL, sends a GET
request to that URL (if the request method is not defined), and produces a promise that resolves the response. Here’s an example below:
fetch("http://example.com/api/endpoint") .then((response) => { // Do something with response }) .catch((err) => { // Handle error here console.log("Unable to fetch -", err); });
You can also change how the fetch
process is carried out by appending an optional object after the URL, which allows you to change things like request methods, request headers, and other options:
fetch("http://example.com/api/endpoint", { method: "POST", // Specify request method headers: { // Customize request header here "Content-Type": "application/json", // . . . }, body: JSON.stringify({ foo: "bar", bar: "foo", }), // . . . }) .then((res) => res.json()) .then((data) => { console.log("Response data:", data); }) .catch((err) => { console.log("Unable to fetch -", err); });
As shown in the updated example above, we’re now sending a POST
request to the example API endpoint with body data foo
and bar
while also setting the content type to application/json
via the headers option. Next, we use the first then
to return the response as JSON and handle the converted JSON response in the second then
.
The fact that the Fetch API now comes prepackaged as an inbuilt Node module is extremely beneficial to the developer community. Some of these benefits include:
Inbuilt fetch for Node.js might mean the end for packages like node-fetch, got, cross-fetch, and many others that were built for the same purpose. This means you won’t have to conduct an npm install
before performing network operations in Node.
Furthermore, node-fetch, currently the most popular fetch package for Node.js, was recently switched to an ESM-only package. This means you’re unable to import it with the Node require()
function. The native Fetch API will make HTTP fetching in Node environments feel much smoother and more natural.
Developers who have previously used the Fetch API on the front end will feel right at home using the inbuilt fetch()
method in Node. It’ll be much simpler and more intuitive than using external packages to achieve the same functionality in a Node environment.
As mentioned previously, the new Fetch implementation is also based on Undici. As a result, you should anticipate improved performance from the Fetch API, as well.
The browser’s Fetch API has some drawbacks in and of itself, and these will undoubtedly be transferred to the new Node.js Fetch implementation:
The Node.js Fetch API does not have built-in support for progress events during file uploads or downloads. Developers who want fine-grained control over monitoring progress may find this limitation difficult to overcome and may need to look into alternative libraries or solutions.
While the Fetch API is stable in newer Node.js versions, projects using earlier Node.js versions may encounter compatibility difficulties. Developers working in environments where upgrading Node.js is not possible may be forced to rely on alternative libraries or develop workarounds.
The Fetch API’s approach to cookie management is based on browser behavior, which may result in unexpected outcomes in a Node.js environment. When dealing with cookies, developers must exercise caution and ensure accurate configuration for specific use cases.
Migrating to the official, stable Node.js Fetch API is pretty straightforward, as outlined below:
First, ensure that your Node.js version is at least v21 or higher. You can check your current Node.js version using the following command:
node -v
If your Node.js version is below version 21, upgrade to the latest version. You can download the latest version directly from the official Node.js website or use a version manager like nvm (Node Version Manager). For example, you can use NVM to list all available versions of Node.js by running the following command:
nvm ls-remote
Once you’ve seen a preferred version, you can then install it by running:
nvm install x.y.z # E.g to install v22.13.0: nvm install v22.13.0
If you were using an external library for making requests in Node.js, such as node-fetch
or another custom HTTP library, you can remove those dependencies now that the stable Fetch API is available natively:
npm uninstall node-fetch
Replace any code that currently utilizes an external library with the native fetch
implementation. Be careful to update the syntax and handle promises correctly.
Here’s what your code looked like before the update:
And what it looks like after you update to use native Fetch:
fetch("https://api.example.com/data") .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error("Error:", error));
As required, review and adjust any options or headers in your requests. The native Fetch API may differ in behavior or provide extra functionality, so consult the official documentation for any specific requirements.
After you’ve made these changes, thoroughly test your application to check that the migration to the native Fetch API didn’t cause any problems. Take great care with error handling, timeouts, and any custom logic associated with HTTP requests.
Many developers encounter errors when trying to use Fetch in Node.js. Let’s look at some of the most common issues and how to resolve them.
A common issue is the ReferenceError: fetch is not defined
, which typically occurs when using Node.js versions older than v18. Fetch was introduced in Node.js v18 as an experimental feature and became stable in v21. To resolve this error, upgrade to Node.js v18 or later, as described in the previous section.
Furthermore, if you’re using an experimental version (v18–v20), you’ll need to run your app with the experimental flag:
node --experimental-fetch example.js
Additionally, for legacy projects where upgrading Node.js isn’t an option, you can use a library like node-fetch to add Fetch support to your project.
Another common error is TypeError: Failed to fetch
, which is usually triggered by network connectivity problems, invalid endpoints, or Cross-Origin Resource Sharing (CORS) restrictions i.e., when the server you’re making the request to doesn’t include the necessary CORS headers to allow requests from the client’s origin.
To resolve this, verify that the endpoint URL is valid. Also ensure the server is configured to allow CORS by adding the appropriate headers or using techniques like proxy middleware.
Similar to the above, TypeError: Cannot read property 'json' of undefined
is another common error when using fetch
. This typically occurs when you attempt to parse a JSON response that is either malformed or not returned as expected by the server.
To resolve, confirm that the endpoint is returning a valid JSON structure. In some cases, the server might return a blob
, bytes
, or text
data instead, which should be accessed using their appropriate methods, as shown below:
fetch('https://api.example.com/data') .then(response => { if (!response.ok) { throw new Error(`Request failed with status: ${response.status}`); } return response.json(); // Use .blob(), .bytes(), or .text() if required }) .then(data => console.log(data)) .catch(error => console.error('Fetch error:', error));
Fetch in Node.js mimics browser-like behavior; however, unlike browsers, it doesn’t automatically handle cookies, i.e., cookies aren’t stored or sent unless you manage them yourself. For example, if an API relies on cookies for authentication, you’ll need to manually include them in your requests or use a library such as fetch-cookie or tough-cookie to manage them.
Fetch doesn’t natively support timeouts, so if a server takes too long to respond, your request might hang indefinitely. To avoid this, you can use AbortController
to set a timeout for your Fetch requests, as shown in the example below:
const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); // Timeout after 5 seconds fetch("http://example.com/api", { signal: controller.signal }) .then((response) => response.json()) .catch((err) => console.log("Fetch error:", err));
This way, you have full control over how long to wait for a response before canceling the request.
The stabilization of the Node.js Fetch API, made possible by the tireless efforts of the Node.js core team and the key role played by the high-performance HTTP client library Undici, represents a big step forward for developers. The stable release also means you can now use it without the fear of bugs or unexpected issues, and you can enjoy a consistent experience when making HTTP requests in both browser and server environments.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Islands integrates React into legacy codebases, enabling modernization without requiring a complete rewrite.
Onlook bridges design and development, integrating design tools into IDEs for seamless collaboration and faster workflows.
JavaScript generators offer a powerful and often overlooked way to handle asynchronous operations, manage state, and process data streams.
webpack’s Module Federation allows you to easily share code and dependencies between applications, helpful in micro-frontend architecture.
6 Replies to "The Fetch API is finally stable in Node.js"
i was searching for the history of XmlHttpRequest, and here i got.
Thanku😊
I’m happy the article was helpful 🙂
for those who can’t get it working, you need to put the `–experimental-fetch` flag before `app.js` instead of after
Thanks for the catch
Hi, I am using node v.19.6.0 and have fetch() working.
What concerns me is the question of https, how does fetch know where the certificates etc. are stored? Previously I was using node-fetch and this info was declared in the sslConfiguredAgent = new https.Agent(options);
How do I set up fetch to use these ssl certificates?
Thanks