What’s New in Node.js 20

Share this article

What's New in Node.js 20

Version 20 of Node.js was released on 18 April 2023. It addresses some issues and criticisms already “solved” by Deno and Bun, including a new permission model and a stable native test runner. This article examines the new options available to developers using the world’s most-used JavaScript runtime.

Contents:

  1. The Node.js Release Schedule
  2. New Permission Model
  3. Native Test Runner
  4. Compiling a Single Executable Application
  5. Updated V8 JavaScript Engine
  6. Miscellaneous Updates

The Node.js Release Schedule

Node.js has a six-month release schedule:

  • The April even-numbered releases (14, 16, 18, etc.) are stable and receive long-term support (LTS) updates for three years.

  • The October odd-numbered release (15, 17, 19, etc.) are more experimental and updates often end after one year.

In general, you should opt for the even-numbered LTS version unless you require a specific feature in an experimental release and intend to upgrade later. That said, Node.js 20 is new and the website advises you continue with version 18 while the development team fixes any late-breaking issues.

Node.js 20 has the following new features …

New Permission Model

Running node somescript.js is not without risk. A script can do anything: delete essential files, send private data to a server, or run a cryptocurrency miner in a child process. It’s difficult to guarantee your own code won’t break something: can you be certain that all modules and their dependencies are safe?

The new (experimental) Node.js Permission Model restricts what script can do. To use it, add the --experimental-permission flag your node command line followed by:

  1. --allow-fs-read to grant read-access to files. You can limit read-access to:

    • specific directories: --allow-fs-read=/tmp/
    • specific files: --allow-fs-read=/home/me/data.json
    • or wildcard file patterns: --allow-fs-read=/home/me/*.json
  2. --allow-fs-write to grant write-access to files with identical directory, file, or wildcard patterns.

  3. --allow-child-process to permit child processes such as executing other scripts perhaps written in other languages.

  4. --allow-worker to permit worker threads, which execute Node.js code in parallel to the main processing thread.

In the following example, somescript.js can read files in the /home/me/data/ directory:

node --experimental-permission --allow-fs-read=/home/me/data/ somescript.js

Any attempt to write a file, execute another process, or launch a web worker raises a ERR_ACCESS_DENIED error.

You can check permissions within your application using the new process.permission object. For example, here’s how to check whether the script can write files:

process.permission.has('fs.write');

Here’s how to check if the script can write to a specific file:

if ( !process.permission.has('fs.write', '/home/me/mydata.json') ) {
  console.error('Cannot write to file');
}

JavaScript permission management was first introduced by Deno, which offers fine-grained control over access to files, environment variables, operating system information, time measurement, the network, dynamically-loaded libraries, and child processes. Node.js is insecure by default unless you add the --experimental-permission flag. This is less effective, but ensures existing scripts continue to run without modification.

Native Test Runner

Historically, Node.js has been a minimal runtime so developers could choose what tools and modules they required. Running code tests required a third-party module such as Mocha, AVA, or Jest. While this resulted in plenty of choices, it can be difficult to make the best decision, and switching tools may not be easy.

Other runtimes took an alternative view and offered built-in tools considered essential for development. Deno, Bun, Go, and Rust all offer built-in test runners. Developers have a default choice but can opt for an alternative when their project has specific requirements.

Node.js 18 introduced an experimental test runner which is now stable in version 20. There’s no need to install a third-party module, and you can create test scripts:

  • in your project’s /test/ directory
  • by naming the file test.js, test.mjs, or test.cjs
  • using test- at the beginning of the filename — such as test-mycode.js
  • using test at the end of the filename with preceding period (.), hyphen (-) or underscore (_) — such as mycode-test.js, mycode_test.cjs, or mycode.test.mjs

You can then import node:test and node:assert and write testing functions:

// test.mjs
import { test, mock } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';

test('my first test', (t) => {
  assert.strictEqual(1, 1);
});

test('my second test', (t) => {
  assert.strictEqual(1, 2);
});

// asynchronous test with mocking
mock.method(fs, 'readFile', async () => 'Node.js test');
test('my third test', async (t) => {
  assert.strictEqual( await fs.readFile('anyfile'), 'Node.js test' );
});

Run the tests with node --test test.mjs and examine the output:

✔ my first test (0.9792ms)
✖ my second test (1.2304ms)
  AssertionError: Expected values to be strictly equal:

  1 !== 2

      at TestContext.<anonymous> (test.mjs:10:10)
      at Test.runInAsyncScope (node:async_hooks:203:9)
      at Test.run (node:internal/test_runner/test:547:25)
      at Test.processPendingSubtests (node:internal/test_runner/test:300:27)
      at Test.postRun (node:internal/test_runner/test:637:19)
      at Test.run (node:internal/test_runner/test:575:10)
      at async startSubtest (node:internal/test_runner/harness:190:3) {
    generatedMessage: false,
    code: 'ERR_ASSERTION',
    actual: 1,
    expected: 2,
    operator: 'strictEqual'
  }

✔ my third test (0.1882ms)
ℹ tests 3
ℹ pass 2
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 72.6767

You can add a --watch flag to automatically re-run tests when the file changes:

node --test --watch test.mjs

You can also run all tests found in the project:

node --test

Native testing is a welcome addition to the Node.js runtime. There’s less need to learn different third-party APIs, and I no longer have an excuse when forgetting to add tests to smaller projects!

Compiling a Single Executable Application

Node.js projects require the runtime to execute. This can be a barrier when distributing applications to platforms or users who can’t easily install or maintain Node.js.

Version 20 offers an experimental feature which allows you to create a single executable application (SEA) that you can deploy without dependencies. The manual explains the process, although it’s a little convoluted:

  1. You must have a project with a single entry script. It must use CommonJS rather than ES Modules.

  2. Create a JSON configuration file used to build your script into a blob which runs inside the runtime. For example, sea-config.json:

    {
      "main": "myscript.js",
      "output": "sea-prep.blob"
    }
    
  3. Generate the blob with node --experimental-sea-config sea-config.json.

  4. According to your OS, you must then copy the node executable, remove the binary’s signature, inject the blob into the binary, re-sign it, and test the resulting application.

While it works, you’re limited to older CommonJS projects and can only target the same OS as you’re using. It’s certain to improve, given the superior Deno compiler can create an executable for any platform in a single command from JavaScript or TypeScript source files.

You should also be aware of the resulting executable’s file size. A single console.log('Hello World'); generates a file of 85MB, because Node.js (and Deno) need to append the whole V8 JavaScript engine and standard libraries. Options to reduce file sizes are being considered, but it’s unlikely to go below 25MB.

Compilation won’t be practical for small command-line tools, but it’s a more viable option for larger projects such as a full web server application.

Updated V8 JavaScript Engine

Node.js 20 includes the latest version of the V8 engine, which includes the following JavaScript features:

Miscellaneous Updates

The following updates and improvements are also available:

Summary

Node.js 20 is a major step forward. It’s a more significant release, and implements some of Deno’s better features.

However, this begs the question: should you use Deno instead?

Deno is great. It’s stable, natively supports TypeScript, reduces development times, requires fewer tools, and receives regular updates. On the downside, it’s been around less time, has fewer modules, and they’re often shallower imitations of Node.js libraries.

Deno and Bun are worth considering for new projects, but there are thousands of existing Node.js applications. Deno and Bun are making it easier to transition code, but there won’t always be a clear advantage for moving away from Node.js.

The good news is we have a thriving JavaScript ecosystem. The runtime teams are learning from each other and rapid evolution benefits developers.

FAQs on the Latest Features in Node.js 20

What are the new features in Node.js 20?

Node.js 20 comes with a host of new features and improvements that enhance its performance and usability. One of the most notable features is the introduction of the V8 JavaScript engine version 9.4, which significantly improves the performance of JavaScript execution. Additionally, Node.js 20 includes updates to the libuv, which provides cross-platform support for asynchronous I/O. This version also introduces experimental support for ECMAScript modules, a new standard for JavaScript modules. Other notable features include improved diagnostics, better support for native addons, and updates to the Node.js API (N-API) for building native addons.

How does Node.js 20 improve performance?

Node.js 20 improves performance in several ways. Firstly, it includes the V8 JavaScript engine version 9.4, which enhances JavaScript execution speed. Secondly, it introduces updates to the libuv, which provides better support for asynchronous I/O, thereby improving the performance of I/O operations. Lastly, Node.js 20 includes improvements to the Node.js API (N-API), which allows for more efficient building of native addons.

What is the V8 JavaScript engine in Node.js 20?

The V8 JavaScript engine is a crucial component of Node.js. It is responsible for executing JavaScript code in the Node.js environment. In Node.js 20, the V8 engine has been updated to version 9.4. This update brings significant improvements in JavaScript execution speed, making Node.js applications faster and more efficient.

What is the libuv in Node.js 20?

libuv is a multi-platform support library with a focus on asynchronous I/O. It was primarily developed for use in Node.js but can also be used in other software projects. In Node.js 20, libuv has been updated to provide better support for asynchronous I/O operations, thereby improving the performance of Node.js applications.

What is the experimental support for ECMAScript modules in Node.js 20?

Node.js 20 introduces experimental support for ECMAScript modules, a new standard for JavaScript modules. This means that developers can now use ECMAScript modules in their Node.js applications, although this feature is still in the experimental stage and may not be fully stable.

How does Node.js 20 improve diagnostics?

Node.js 20 includes several improvements to diagnostics. These include better error handling, improved trace events, and enhanced debugging capabilities. These improvements make it easier for developers to identify and fix issues in their Node.js applications.

What is the Node.js API (N-API) in Node.js 20?

The Node.js API (N-API) is an API for building native addons for Node.js. In Node.js 20, the N-API has been updated to provide better support for building native addons. This means that developers can now build more efficient and performant native addons for their Node.js applications.

How does Node.js 20 support native addons?

Node.js 20 includes updates to the Node.js API (N-API) that improve support for building native addons. Native addons are dynamically-linked shared objects, written in C or C++, that can be loaded into Node.js using the require() function, and used just as if they were an ordinary Node.js module.

What are the system requirements for Node.js 20?

Node.js 20 can be installed on a variety of systems. It supports multiple operating systems including Linux, macOS, and Windows. The specific system requirements may vary depending on the operating system. It is recommended to check the official Node.js website for the most accurate and up-to-date information.

How can I upgrade to Node.js 20?

Upgrading to Node.js 20 is a straightforward process. You can download the latest version from the official Node.js website and follow the installation instructions provided. If you already have Node.js installed, you can use the Node Version Manager (NVM) to easily upgrade to the latest version.

Craig BucklerCraig Buckler
View Author

Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.

DenoLearn-Node-JSnode
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form