Using Promise.withResolvers in Node.js Tests

Known for his open source and JavaScript security initiatives, Liran Tal is an award-winning software developer, security researcher, and open source champion in the JavaScript community. He's an internationally recognized GitHub Star, acknowledged for his open source advocacy, and has received the OpenJS Foundation's Pathfinder for Security for his work on Node.js security. His contributions to developer security education include leading OWASP projects, building supply chain security tools, participation in CNCF and OpenSSF initiatives, and authoring books such as O'Reilly's Serverless Security. He leads the developer advocacy team at Snyk.io and is on a mission to empower developers with better application security skills.
If you often encounter scenarios where managing asynchronous operations efficiently is crucial but you’re left wondering how exactly to do so (someone said event emitter? callback patterns??) then there’s good news and it’s called Promise.withResolvers.
One such scenario is running nested tests that have asynchronous code in Node.js and I want to visit that in this article. I’ll show you different ways to use the Promise.withResolvers API, a cool new JavaScript way introduced in Node.js v22.0.0, which simplifies the handling of nested tests by providing a more elegant way to signal their completion.
Use case: Promise.withResolvers in Tests
Another use-case for Promise.withResolvers is in tests. Here’s an example from the Node.js core tests when tests are nested and you want to signal the end of all tests:
import { test } from 'node:test';
test('foo', t => { t.test('bar', t => { t.plan(1) t.assert.equal(1, 1) })
t.end() })
In the above example, t.end() is used to signal the end of the test. HOWEVER, if you ran that code it wouldn’t work. Why? there’s no t.end() in the native Node.js test runner, so that code snippet above is really just a pseudo-code example.
How would you do it instead? probably something like this:
import { test } from 'node:test';
test('foo', async (t) => { await Promise.all([ t.test('bar 1', async (t) => { assert.equal(1, 1); }), t.test('bar 2', async (t) => { assert.equal(1, 1); }) ]); });
Today is the first time I have ever had a need for
Promise.withResolvers. It’s a rather effective hack around not havingt.end.
- James Sumners shared on X
But, Promise withResolvers to the rescue! 🦸♀️
If you want to use Promise instead of the pseudo-code t.end() or our Promise.all() wrapper, you can use Promise.withResolvers like so:
test('foo', async t => { const { promise, resolve } = Promise.withResolvers();
let completedTests = 0; const totalTests = 2;
const checkCompletion = () => { completedTests += 1; if (completedTests === totalTests) { resolve(); } };
t.test('bar 1', t => { t.assert.equal(1, 1); checkCompletion(); });
t.test('bar 2', t => { t.assert.equal(1, 1); checkCompletion(); });
await promise; });
In the above example, we’re using Promise.withResolvers to signal the end of the test. We’re keeping track of the number of completed tests using a counter and when all tests are completed, we call resolve() to signal the end of the test.
Another refactor of the above code could be to use Promise.all():
import { test } from 'node:test'; import { setTimeout } from 'node:timers/promises'
test('foo', async t => { const { promise, resolve } = Promise.withResolvers();
const subtests = [ t.test('bar 1', async t => { await setTimeout(2500); t.assert.equal(1, 1); }), t.test('bar 2', async t => { await setTimeout(5500); t.assert.equal(1, 1); }) ];
Promise.all(subtests).then(resolve);
await promise; });
Node.js Promise.withResolvers API version limitations
The Promise.withResolvers API is available in Node.js v22.0.0 and later. If you’re using an older version of Node.js, you can’t use this API directly.




