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:
- The Node.js Release Schedule
- New Permission Model
- Native Test Runner
- Compiling a Single Executable Application
- Updated V8 JavaScript Engine
- 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:
-
--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
- specific directories:
-
--allow-fs-write
to grant write-access to files with identical directory, file, or wildcard patterns. -
--allow-child-process
to permit child processes such as executing other scripts perhaps written in other languages. -
--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
, ortest.cjs
- using
test-
at the beginning of the filename — such astest-mycode.js
- using
test
at the end of the filename with preceding period (.
), hyphen (-
) or underscore (_
) — such asmycode-test.js
,mycode_test.cjs
, ormycode.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:
-
You must have a project with a single entry script. It must use CommonJS rather than ES Modules.
-
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" }
-
Generate the blob with
node --experimental-sea-config sea-config.json
. -
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:
-
String.prototype.isWellFormed(): returns
true
when a string is well-formed and doesn’t contain lone (unpaired) surrogate characters. -
String.prototype.toWellFormed(): returns a well-formed string that fixes lone surrogate character issues.
-
A new regular expression
v
flag which addresses issues with casing of Unicode characters.
Miscellaneous Updates
The following updates and improvements are also available:
-
performance improvements to the URL, native fetch(), and EventTarget APIs
-
ES module loading improvements, including experimental support for import.meta.resolve(), which can scope a module’s file path reference to a URL string
-
Web Crypto API interoperability improvements
-
Further progress on the WebAssembly System Interface (WASI), which grants sandboxed WASM applications access to the operating system
-
Official support for ARM64 on Windows
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 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.