Node.js 20 is now available!
The Node.js Project
We're excited to announce the release of Node.js 20! Highlights include the new Node.js Permission Model,a synchronous import.meta.resolve
, a stable test_runner, updates of the V8 JavaScript engine to 11.3, Ada to 2.0,
and more!
The project continues to make progress across a number of areas, with many new features and fixes flowing into existing LTS releases. For that reason, the changes outlined in the changelog for Node.js 20 only represent a small subset of the features and work since the last major release. This blog post will add some additional context on the larger body of work in relation to those changes.
You can read more about our release policy at https://github.com/nodejs/release.
To download Node.js 20.0.0, visit: https://nodejs.org/download/current/. You can find the release post at https://nodejs.org/blog/release/v20.0.0, which contains the full list of commits included in this release.
As a reminder, Node.js 20 will enter long-term support (LTS) in October, but until then, it will be the "Current" release for the next six months. We encourage you to explore the new features and benefits offered by this latest release and evaluate their potential impact on your applications.
Notable Changes
Permission Model
The Node.js Permission Model is an experimental mechanism for restricting access to specific resources during execution.
In this first release containing the Permission Model, the features come with the following abilities:
- Restrict access to the file system (read and write)
- Use
--allow-fs-read
and--allow-fs-write
- Use
- Restrict access to
child_process
- Restrict access to
worker_threads
- Use
--allow-worker
- Use
- Restrict access to native addons (same as
--no-addons
flag)
The available permissions are documented by the
--experimental-permission
flag.
When starting Node.js with --experimental-permission
, the ability to access the file system, spawn processes, and use node:worker_threads
will be restricted.
Developers using Node.js now have more control over file system access with the introduction of the --allow-fs-read
and --allow-fs-write
flags. These experimental features allow for more granular control over which parts of the file system can be accessed by Node.js processes.
To enable these flags, developers can use the --experimental-permission
flag along with the desired permissions. For example, running the following command allows for both read and write access to the entire file system:
$ node --experimental-permission --allow-fs-read=* --allow-fs-write=* index.js
Developers can also specify specific paths for file system access by passing in comma-separated values to the flags. For example, the following command allows for write access to the /tmp/
folder:
$ node --experimental-permission --allow-fs-write=/tmp/ --allow-fs-read=/home/index.js index.js
Wildcard patterns can also be used to allow for access to multiple files or folders at once. For example, the following command allows for read access to all files and folders in the /home/
directory that start with test
:
$ node --experimental-permission --allow-fs-read=/home/test* index.js
When the Permission Model is enabled, the new permission
property of the process
object can be used to check if a certain permission has been granted at runtime.
var process: NodeJS.Process
process.NodeJS.Process.permission: NodeJS.ProcessPermission
This API is available through the [--permission](https://nodejs.org/api/cli.html#--permission) flag.
`process.permission` is an object whose methods are used to manage permissions for the current process.
Additional documentation is available in the [Permission Model](https://nodejs.org/api/permissions.html#permission-model).permission.NodeJS.ProcessPermission.has(scope: string, reference?: string): boolean
Verifies that the process is able to access the given scope and reference.
If no reference is provided, a global scope is assumed, for instance, `process.permission.has('fs.read')`
will check if the process has ALL file system read permissions.
The reference has a meaning based on the provided scope. For example, the reference when the scope is File System means files and folders.
The available scopes are:
* `fs` - All File System
* `fs.read` - File System read operations
* `fs.write` - File System write operations
* `child` - Child process spawning operations
* `worker` - Worker thread spawning operation
```js
// Check if the process has permission to read the README file
process.permission.has('fs.read', './README.md');
// Check if the process has read permission operations
process.permission.has('fs.read');
```has('fs.write'); // true
var process: NodeJS.Process
process.NodeJS.Process.permission: NodeJS.ProcessPermission
This API is available through the [--permission](https://nodejs.org/api/cli.html#--permission) flag.
`process.permission` is an object whose methods are used to manage permissions for the current process.
Additional documentation is available in the [Permission Model](https://nodejs.org/api/permissions.html#permission-model).permission.NodeJS.ProcessPermission.has(scope: string, reference?: string): boolean
Verifies that the process is able to access the given scope and reference.
If no reference is provided, a global scope is assumed, for instance, `process.permission.has('fs.read')`
will check if the process has ALL file system read permissions.
The reference has a meaning based on the provided scope. For example, the reference when the scope is File System means files and folders.
The available scopes are:
* `fs` - All File System
* `fs.read` - File System read operations
* `fs.write` - File System write operations
* `child` - Child process spawning operations
* `worker` - Worker thread spawning operation
```js
// Check if the process has permission to read the README file
process.permission.has('fs.read', './README.md');
// Check if the process has read permission operations
process.permission.has('fs.read');
```has('fs.write', '/home/nodejs/protected-folder'); // true
It is important to note that these features are still experimental and may change in future releases of Node.js
Furthermore can be found in Permission Model documentation.
The Permission Model was a contribution by Rafael Gonzaga in #44004.
Custom ESM loader hooks nearing stable
Custom ES module lifecycle hooks supplied via loaders (--experimental-loader=./foo.mjs
) now run in a dedicated thread, isolated from the main thread. This provides a separate scope for loaders and ensures no cross-contamination between loaders and application code.
In alignment with browser behavior, import.meta.resolve()
now returns synchronously; note that resolve
hooks in user loaders can remain async if the loader author desires, and import.meta.resolve
will still return synchronously in application code.
These changes were the last outstanding items before marking ESM loaders as stable. Once some time has gone by without significant bugs reported by the community, we intend to mark the loaders flag, import.meta.resolve
and the resolve
and load
hooks as stable. This should enable more widespread adoption of ESM, as important constituencies such as instrumentation vendors will have a stable API on which to build analytics and reporting libraries.
Contributed by Anna Henningsen, Antoine du Hamel, Geoffrey Booth, Guy Bedford, Jacob Smith, and Michaël Zasso in #44710.
V8 11.3
As per usual a new version of the V8 engine is included in Node.js (updated to version 11.3, which is part of Chromium 113) bringing improved performance and new language features including:
- String.prototype.isWellFormed and toWellFormed
- Methods that change Array and TypedArray by copy
- Resizable ArrayBuffer and growable SharedArrayBuffer
- RegExp v flag with set notation + properties of strings
- WebAssembly Tail Call
The V8 update was a contribution by Michaël Zasso in #47251.
Stable Test Runner
The recent update to Node.js, version 20, includes an important change to the test_runner module. The module has been marked as stable after a recent update. The stable test runner includes the building blocks for authoring and running tests, including:
describe
,it
/test
and hooks to structure test files- mocking
- watch mode
node --test
for running multiple test files in parallel
The test runner also includes some parts that are not yet stable, including reporters and code coverage.
This is a simple example of using the test runner:
import { function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)
The `test()` function is the value imported from the `test` module. Each
invocation of this function results in reporting the test to the `TestsStream`.
The `TestContext` object passed to the `fn` argument can be used to perform
actions related to the current test. Examples include skipping the test, adding
additional diagnostic information, or creating subtests.
`test()` returns a `Promise` that fulfills once the test completes.
if `test()` is called within a suite, it fulfills immediately.
The return value can usually be discarded for top level tests.
However, the return value from subtests should be used to prevent the parent
test from finishing first and cancelling the subtest
as shown in the following example.
```js
test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});
```
The `timeout` option can be used to fail the test if it takes longer than `timeout` milliseconds to complete. However, it is not a reliable mechanism for
canceling tests because a running test might block the application thread and
thus prevent the scheduled cancellation.test, const mock: test.MockTracker
mock } from 'node:test';
import function assert(value: unknown, message?: string | Error): asserts value
An alias of
{@link
ok
}
.assert from 'node:assert';
import module "node:fs"
fs from 'node:fs';
const mock: test.MockTracker
mock.test.MockTracker.method<typeof fs, "readFile", () => Promise<string>>(object: typeof fs, methodName: "readFile", implementation: () => Promise<string>, options?: test.MockFunctionOptions): test.Mock<...> (+3 overloads)
This function is used to create a mock on an existing object method. The
following example demonstrates how a mock is created on an existing object
method.
```js
test('spies on an object method', (t) => {
const number = {
value: 5,
subtract(a) {
return this.value - a;
},
};
t.mock.method(number, 'subtract');
assert.strictEqual(number.subtract.mock.calls.length, 0);
assert.strictEqual(number.subtract(3), 2);
assert.strictEqual(number.subtract.mock.calls.length, 1);
const call = number.subtract.mock.calls[0];
assert.deepStrictEqual(call.arguments, [3]);
assert.strictEqual(call.result, 2);
assert.strictEqual(call.error, undefined);
assert.strictEqual(call.target, undefined);
assert.strictEqual(call.this, number);
});
```method(module "node:fs"
fs, 'readFile', async () => 'Hello World');
function test(name?: string, fn?: test.TestFn): Promise<void> (+3 overloads)
The `test()` function is the value imported from the `test` module. Each
invocation of this function results in reporting the test to the `TestsStream`.
The `TestContext` object passed to the `fn` argument can be used to perform
actions related to the current test. Examples include skipping the test, adding
additional diagnostic information, or creating subtests.
`test()` returns a `Promise` that fulfills once the test completes.
if `test()` is called within a suite, it fulfills immediately.
The return value can usually be discarded for top level tests.
However, the return value from subtests should be used to prevent the parent
test from finishing first and cancelling the subtest
as shown in the following example.
```js
test('top level test', async (t) => {
// The setTimeout() in the following subtest would cause it to outlive its
// parent test if 'await' is removed on the next line. Once the parent test
// completes, it will cancel any outstanding subtests.
await t.test('longer running subtest', async (t) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
});
});
```
The `timeout` option can be used to fail the test if it takes longer than `timeout` milliseconds to complete. However, it is not a reliable mechanism for
canceling tests because a running test might block the application thread and
thus prevent the scheduled cancellation.test('synchronous passing test', async t: test.TestContext
t => {
// This test passes because it does not throw an exception.
function assert(value: unknown, message?: string | Error): asserts value
An alias of
{@link
ok
}
.assert.function assert.strictEqual<"Hello World">(actual: unknown, expected: "Hello World", message?: string | Error): asserts actual is "Hello World"
Tests strict equality between the `actual` and `expected` parameters as
determined by [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is).
```js
import assert from 'node:assert/strict';
assert.strictEqual(1, 2);
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
//
// 1 !== 2
assert.strictEqual(1, 1);
// OK
assert.strictEqual('Hello foobar', 'Hello World!');
// AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
// + actual - expected
//
// + 'Hello foobar'
// - 'Hello World!'
// ^
const apples = 1;
const oranges = 2;
assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
// AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical
```
If the values are not strictly equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a
default error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown
instead of the `AssertionError`.strictEqual(await module "node:fs"
fs.function readFile(path: fs.PathOrFileDescriptor, options: ({
encoding?: null | undefined;
flag?: string | undefined;
} & EventEmitter<T extends EventMap<T> = DefaultEventMap>.Abortable) | undefined | null, callback: (err: NodeJS.ErrnoException | null, data: NonSharedBuffer) => void): void (+3 overloads)
Asynchronously reads the entire contents of a file.
```js
import { readFile } from 'node:fs';
readFile('/etc/passwd', (err, data) => {
if (err) throw err;
console.log(data);
});
```
The callback is passed two arguments `(err, data)`, where `data` is the
contents of the file.
If no encoding is specified, then the raw buffer is returned.
If `options` is a string, then it specifies the encoding:
```js
import { readFile } from 'node:fs';
readFile('/etc/passwd', 'utf8', callback);
```
When the path is a directory, the behavior of `fs.readFile()` and
{@link
readFileSync
}
is platform-specific. On macOS, Linux, and Windows, an
error will be returned. On FreeBSD, a representation of the directory's contents
will be returned.
```js
import { readFile } from 'node:fs';
// macOS, Linux, and Windows
readFile('<directory>', (err, data) => {
// => [Error: EISDIR: illegal operation on a directory, read <directory>]
});
// FreeBSD
readFile('<directory>', (err, data) => {
// => null, <data>
});
```
It is possible to abort an ongoing request using an `AbortSignal`. If a
request is aborted the callback is called with an `AbortError`:
```js
import { readFile } from 'node:fs';
const controller = new AbortController();
const signal = controller.signal;
readFile(fileInfo[0].name, { signal }, (err, buf) => {
// ...
});
// When you want to abort the request
controller.abort();
```
The `fs.readFile()` function buffers the entire file. To minimize memory costs,
when possible prefer streaming via `fs.createReadStream()`.
Aborting an ongoing request does not abort individual operating
system requests but rather the internal buffering `fs.readFile` performs.readFile('a.txt'), 'Hello World');
});
Contributed by Colin Ihrig in #46983
Performance
With the newly formed Node.js Performance team, there has been a renewed focus on performance since the last Major release. Node.js 20 includes many improvements to the fundamental parts of the runtime including URL
, fetch()
, and EventTarget
.
The cost of initializing EventTarget
has been cut by half, providing faster access to all subsystems using it. Additionally, V8 Fast API calls have been leveraged to improve performance in APIs such as URL.canParse()
and timers.
Node.js 20 includes specific changes, such as the updated version 2.0 of Ada, a fast and spec-compliant URL parser written in C++.
Looking forward to new ways to improve performance we are currently working on reducing the cost of being specification compliant by refactoring to get rid of brand validations checks on streams, URL
, URLSearchParams
, and String Decoder. This helps support our general goal of being specification compliant where it makes sense.
If you have a passion for performance and Node.js, we are actively looking for contributors for our performance team.
Preparing single executable apps now requires injecting a Blob
The project has been working on support for Single Executable Applications (SEA) over the last year with initial support landing recently. The team continues to refine the approach as the feature is still Experimental. In Node.js 20, building a single executable app now requires injecting a blob prepared by Node.js from a JSON config instead of injecting the raw JS file.
Example:
sea-config.json
{
"main": "hello.js",
"output": "sea-prep.blob"
}
This writes the blob to the sea-prep.blob
file.
node --experimental-sea-config sea-config.json
This blob can now be injected into the binary.
This change was made to allow the the possibility of embedding multiple co-existing resources into the SEA (Single Executable Apps) which opens up new use cases.
Contributed by Joyee Cheung in #47125
Web Crypto API
The project works toward interoperability with other JavaScript environments. As an example of that in Node.js 20, the Web Crypto API functions' arguments are now coerced and validated as per their WebIDL definitions like in other Web Crypto API implementations. This further improves interoperability with other implementations of Web Crypto API.
This change was made by Filip Skokan in #46067.
Official support for ARM64 Windows
Node.js has broad platform and architecture support and people seem to want it to run everywhere. We are happy to share that Node.js now includes binaries for ARM64 Windows, allowing for native execution on the platform. The MSI, zip/7z packages, and executable are available from the Node.js download site along with all other platforms. The CI system was updated and all changes are now fully tested on ARM64 Windows, to prevent regressions and ensure compatibility.
ARM64 Windows was upgraded to tier 2 support by Stefan Stojanovic in #47233.
Progress on Web Assembly System Interface (WASI)
The project continues to work on the WASI implementation within Node.js. Some notable progress is that although it is experimental a command line option is no longer required to enable WASI. This should make it easier to consume. As the team working on WASI looks forward to preview2
a few changes were also made to plan for the future. That included adding a version
option when new WASI()
is called. In the 20.x release the version
is required and has no default value. This is important to that as new versions are supported applications don't default to what may be an obsolete version. This does mean, however, that any code that relied on the default for the version will need to be updated to request a specific version.
If you have a interest in using WASI in Node.js or uvwasi which is used outside of Node.js itself, the team would love additional contributors.
Call to action!
Try out the new Node.js 20 release! We’re always happy to hear your feedback. Testing your applications and modules with Node.js 20 helps to ensure the future compatibility of your project with the latest Node.js changes and features.
Also of note is that Node.js 14 will go End-of-Life in April 2023, so we advise you to start planning to upgrade to Node.js 18 (LTS) or Node.js 20 (soon to be LTS).
Please, consider that Node.js 16 (LTS) will go End-of-Life in September 2023, which was brought forward from April 2024 to coincide with the end of support of OpenSSL 1.1.1. You can read more details about that decision at https://nodejs.org/blog/announcements/nodejs16-eol/.
Looking to the future, the Next-10 team is running a survey to gather info from the ecosystem. Help shape the future of Node.js by participating. Submit your feedback here.