javascript

Node.js 18 Release: What's New

Nate Anderson

Nate Anderson on

Node.js 18 Release: What's New

The Node.js team announced the release of version 18 on April 19. Node.js 18 has some significant new features that Node developers should be aware of, including:

  • The upgrade of the V8 engine to version 10.1.
  • An experimental test runner module.
  • Most excitingly, experimental support for browser APIs like fetch and Streams.

In this article, we'll look at some of the major highlights from this release.

Long-term Support Release Status for Node.js 18

Node.js 18 is a long-term support (LTS) release, so it will continue to receive updates for 30 months. Because this is a new release, it is considered the current release, meaning development work on this version is ongoing, and we should expect regular updates.

After six months, in October 2022, it will become the "active LTS" release, because version 19 will be released. The active LTS release will continue to receive updates, which may be either bug fixes, security patches, or new features backported from the current release.

Once an LTS release switches to maintenance mode, typically at least two years after release, it will receive security patches and bugfixes almost exclusively.

V8 Engine Upgrade in Node.js 18

The V8 engine is the JavaScript runtime developed by Google for Chrome, and used by Node.js to execute JavaScript. Any changes to how we use the JavaScript language inside Node.js must come through the V8 engine.

V8's upgrade to 10.1 means that the Array methods findLast and findLastIndex are now available in Node.js. These methods return the last item or index of an item in an array that passes a filter function.

Array.findLast works just like Array.find, except it begins at the end of the array and traverses it in reverse order. Array.findLastIndex simply returns the item's index, rather than the item itself.

These methods both take a higher-order function as an argument and call this function on each element in the list, returning from the method call when the function returns true.

The 10.1 release of the V8 engine also includes minor performance improvements, which you can read about in the release notes on the V8 project blog.

Node.js 18 Has A Built-in Test Runner

Node.js has included the assert library since very early days, making it fairly easy to write simple tests without a third-party library. But good test suites grow quickly, and most projects today depend on a test runner library like Jest, Mocha, Jasmine, or Ava.

These libraries make it easier to organize tests into logical groups, visualize test outcomes, collect coverage data, and simplify setup and teardown.

The node:test module included in Node.js version 18 adds a lot of functionality for which you would previously need one of these modules.

The built-in test runner supports subtests, test skipping, limited test runs, and callback tests. Here are a few examples of these features in action:

import assert from "assert";
import test from "node:test";

Notice the "node:" prefix, distinguishing this module from any user-created packages named "test".

test("Concatenate user's full name", (t) => {
  const user = new User("John", "Doe");
  assert.strictEqual(user.fullName(), "John Doe");
});

The test import is a function used to define tests. The first argument is a string describing the test, and the second is the function containing the test logic. This API is roughly the same as Ava's test, and takes the place of both it and describe from Jest.

It covers the same ground as Jest's describe because it can also define test groups, with individual tests (or even test subgroups) defined by calling t.test within the callback function.

For example, we can create a test group for serialization of a class and individual tests for each type of supported serialization.

test("User class serialization", (t) => {
    const user = new User("John", "Doe");
 
    await t.test("Serialize to string", (t) => {
        const str = user.toString();
        assert.ok(str.includes("John"));
        assert.ok(str.includes("Doe"));
        ...
    });
 
    await t.test("Serialize to JSON", (t) => {
        const userJson = user.toJSON();
        assert.doesNotThrow(() => JSON.parse(userJson));
        ...
    });
});

Now, the string passed to test describes a test group, and each call to t.test defines an individual test. Note that t.test returns a promise, hence the awaits in the example.

While it's exciting to see these testing features embraced by the Node.js standard library, I think it's reasonable to expect most projects to use a third-party testing module still.

Many Node devs already have a favorite test library, and it's likely to be a while before the node:test core module supports all the features that third-party libraries offer.

New Browser-compatible APIs in Node.js 18

Commonly supported browser APIs like fetch and Streams are among the most significant additions to Node.js 18.

In the browser, fetch replaced the cumbersome XMLHttpRequest with a flexible, terse HTTP client. The Streams API allows us to perform incremental processing of data that's received from or sent to a network.

It's important to note that these APIs are still considered experimental in Node.js 18. It may be wise to avoid their use in mission-critical applications for the moment, as breaking API updates are not out of the question.

Node.js Now Supports Fetch API

Adding support for fetch to Node.js allows us to make HTTP requests concisely without:

  • Third-party libraries like axios or node-fetch (which exposes an API roughly equivalent to the new built-in global)
  • Relying on the more complicated http and https packages to make requests from Node projects.

The existing http and https packages are very flexible and support advanced functionality. However, the fetch global function is much more concise; writing Node.js code will feel more natural to developers used to writing JavaScript for the browser. Additionally, because Node.js and modern browsers now share this API, it is easier to write code to run in both environments.

Here's how we would write an HTTPS request in Node.js using only first-party modules before the addition of the fetch API:

import https from "https";
 
const data = {
  nameFirst: "John",
  nameLast: "Doe",
};
 
let responseData = "";
 
const req = https.request(
  "https://example.org/user",
  { method: "POST" },
  (res) => {
    res.on("data", (data) => {
      responseData += data;
    });
 
    res.on("error", (error) => console.error(error));
  }
);
 
req.end(JSON.stringify(data), "utf-8", () => {
  console.log(`Response data: ${responseData}`);
});

Notice the heavy reliance on callbacks and the fact that the https module does not support promises. We have to register handlers for response data and errors, and be careful not to use the response data variable before the request completes.

While this is a somewhat awkward example, it highlights the difficulty of using the https and http modules in Node, and explains why most developers choose to use a third-party module like Axios, request, or node-fetch.

If we were to make the same request in Node 18 using the fetch API, it would look something like this:

const data = {
  nameFirst: "John",
  nameLast: "Doe",
};
 
try {
  const response = await fetch("https://example.org/user", {
    method: "POST",
    body: JSON.stringify(data),
  });
 
  const responseJson = await response.json();
  console.log(`Response data: ${responseJson}`);
} catch (error) {
  console.error(error);
}

This version is decidedly more terse and familiar to JavaScript devs used to modern language features. I/O operations return promises, errors are thrown and caught, and the code reads synchronously from top to bottom.

While there will certainly still be use cases for the http and https packages and their lower-level capabilities, I think most devs will prefer the new fetch global function for daily use.

Support for Streams API

The Streams API broadly describes a set of interfaces such as ReadableStream and WritableStream, which allow us to process data incrementally before a whole object loads in memory. The Streams API is actually a prerequisite for fetch compatibility because the body property of a fetch response is a ReadableStream.

A common use case for streams is that we want to use a large amount of data, but don't need the entirety of the data to operate on: think multimedia applications or other real-time apps.

While Node.js has always supported these use cases, the availability of the same high-level APIs in both the client and server environments may make developing these applications easier.

Wrap Up: Ecosystem Implications and Node.js 18 Release Notes

One of the selling points of Node.js, and more broadly, JavaScript-based backends, is that full-stack engineers can work at all levels of a web application's stack without switching languages.

It's clear that the Node team is keen on playing to this advantage through Node.js 18's new features, continuing to narrow the gap between JavaScript in the browser and JavaScript on the server. When we consider the increased prevalence of code that needs to run in both environments, such as server-side rendered websites, it's encouraging to see that gap continue to close.

There have been several smaller changes in Node.js 18, such as changes to internationalization APIs, formalization of the Blob and BroadcastChannel APIs, and more.

If you want to dig into the full set of changes in this version, you should take a look at the official Node.js 18 release notes.

I hope you enjoy trying out the new features in this exciting new LTS release of Node.js. Happy coding!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

Share this article

RSS
Nate Anderson

Nate Anderson

Our guest author Nate Anderson is a software engineer working on new products at CARD.com, a tech company in the United States providing personalized payment solutions like premium banking. He believes the key to great technology is a close relationship between product and engineering, and is excited about tech like Go, TypeScript, GraphQL, and serverless computing.

-> All articles by Nate Anderson-> Become an AppSignal author

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps