Node.js 16: Timers Promises and Apple Silicon support

Felipe Hlibco

Node.js 16 drops April 20th. I’ve been poking around the dev branch and the pre-release notes, and there’s enough here to warrant a proper look. Not a paradigm shift — more like a collection of practical fixes for real friction points.

Here’s what stands out.

Timers Promises Goes Stable #

This is the one I’m most excited about. The Timers Promises API has been experimental since Node.js 15, and it graduates to stable in v16.

If you’ve ever written util.promisify(setTimeout) and felt a little embarrassed about it — yeah, this is for you.

// Before: the util.promisify dance
const { promisify } = require('util');
const sleep = promisify(setTimeout);
await sleep(1000);

// Node.js 16: native promise-based timers
const { setTimeout } = require('timers/promises');
await setTimeout(1000);

Small thing. But small things compound. The timers/promises module gives you promise-returning versions of setTimeout, setInterval, and setImmediate. No wrappers, no third-party packages, no gymnastics.

The setInterval variant is particularly nice because it returns an async iterator:

const { setInterval } = require('timers/promises');

// Poll every 5 seconds, stop after 30 seconds
const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000);

for await (const _ of setInterval(5000, null, { signal: controller.signal })) {
  const status = await checkDeploymentStatus();
  if (status === 'complete') break;
}

That pattern — polling with a timeout — used to require recursive setTimeout or a library. Now it’s five lines with built-in cancellation via AbortController. Clean.

Apple Silicon Binaries #

Node.js 16 will be the first release to ship prebuilt Apple Silicon binaries. They’re fat binaries covering both Intel (x64) and ARM (arm64) architectures, so the same download works on both.

I switched to an M1 MacBook in January. Running Node through Rosetta 2 has been… fine, actually. Surprisingly fine. But native execution is still noticeably faster for CPU-heavy operations like bundling or running large test suites. Benchmarks I’ve seen show 30-40% improvement in V8 execution performance on native ARM compared to translated x64.

For teams that haven’t migrated to Apple Silicon yet, this removes one of the remaining blockers. No more compiling from source or relying on community ARM builds. The official Node.js release covers it.

V8 9.0 #

The V8 engine bump to 9.0 brings a few features worth knowing:

RegExp match indices (/d flag) give you start/end positions for captured groups. Useful if you’re doing text parsing or building syntax highlighters:

const match = /(?<year>\d{4})-(?<month>\d{2})/d.exec('2021-04');
console.log(match.indices.groups);
// { year: [0, 4], month: [5, 7] }

Intl.DisplayNames improvements expand coverage for language, region, and script display names. Handy for internationalization work — something I deal with regularly at TaskRabbit as we’ve been expanding into European markets.

There are also general performance improvements to the V8 engine, though those tend to be incremental rather than dramatic. The biggest wins are usually in garbage collection tuning and JIT compilation optimizations that you benefit from without changing any code.

Web Crypto API (Experimental) #

Node.js 16 adds an experimental implementation of the Web Crypto API. This aligns Node’s crypto capabilities with the browser standard, which is a theme the Node.js team has been pursuing for a while now (Streams API, AbortController, etc.).

const { webcrypto } = require('crypto');
const { subtle } = webcrypto;

const key = await subtle.generateKey(
  { name: 'AES-GCM', length: 256 },
  true,
  ['encrypt', 'decrypt']
);

The practical benefit here is code portability. If you’re building isomorphic libraries that need crypto operations on both client and server, having the same API on both sides reduces the branching logic. It’s experimental, so I wouldn’t rely on it in production yet, but the direction is right.

LTS Timeline #

Node.js 16 enters “Current” status on April 20th and is scheduled for LTS promotion in October 2021. That’s the standard cadence: six months as Current (where breaking changes can still land in minor versions), then LTS with only backports and security fixes.

If you’re still on Node.js 12 — and plenty of teams are — the upgrade path runs through 14 LTS (which is solid and well-tested at this point). I’d plan the jump to 16 LTS once it’s promoted in October, giving the ecosystem a few months to catch up with any native addon changes.

Node.js 15 reaches end-of-life in June, so if you’re running that in production (and you really shouldn’t be; odd-numbered releases don’t get LTS), start planning the move now.

What I’m Watching #

The convergence between Node.js APIs and browser Web APIs continues to be the most interesting long-term trend. Timers Promises, Web Crypto, AbortController, Streams — Node is systematically adopting web platform standards. For teams writing TypeScript that targets both browser and server, this reduces the impedance mismatch in meaningful ways.

Node.js 16 isn’t a landmark release. It’s a solid, practical one. And sometimes that’s exactly what you want from your runtime.