An async test should control the event it is testing. Sleeping for 20 milliseconds and hoping another operation completed can pass on one machine and fail under load. A deferred promise gives the test an explicit release point.
A test can first observe what has started, release one job, await an explicit notification that another started, and only then inspect the next state. That checks causality. Merely awaiting Promise.resolve once is not a general “flush all async work” operation; it only schedules another promise reaction.
Our pool tests use a separate third-started gate to prove a slot was refilled before releasing the first job. They also verify the outer promise has not settled while an active peer remains blocked after a failure. Test both the order and the lifecycle boundary, not just the final array.
Predict, run, and explain
Write the output and trace the important state changes before checking it. Then run the program. Each editor is independent. Async examples end with main() so this editor waits for their work.
async function main() {
function deferred() {
let resolve;
const promise = new Promise(done => {
resolve = done;
});
return { promise, resolve };
}
const gate = deferred();
let finished = false;
const work = gate.promise.then(() => {
finished = true;
});
console.log(finished);
gate.resolve();
await work;
console.log(finished);
}
main();Check your prediction
false
true Wait for a meaningful event rather than elapsed time
A controlled promise lets a test hold one operation at a chosen point. The test should observe the pending state, release the gate, then await the actual operation. Sleeping for an arbitrary interval hides whether the state changed for the reason you intended.
For a pool refill test, use a notification from the next mapper invocation. A microtask turn is not evidence that an arbitrary chain completed. Test causal events through the API rather than trying to flush every possible asynchronous operation.
async function main() {
let release;
const gate = new Promise(resolve => {
release = resolve;
});
const events = [];
const work = gate.then(() => {
events.push("done");
});
console.log(events.length);
release();
await work;
console.log(events.join(","));
}
main();Check your prediction
0
done The first observation proves work is still held. The awaited promise in the second observation is the one that performs the effect, not an unrelated promise resolved nearby.
async function main() {
let notify;
const started = new Promise(resolve => {
notify = resolve;
});
async function job() {
notify();
return 25;
}
const work = job();
await started;
console.log("start observed");
console.log(await work);
}
main();Check your prediction
start observed
25 A separate notification can prove a phase began. In a real test it can notify that the third pool job started while another is still held, distinguishing refill from batch behavior.
Try the rule on new inputs
Create separate started and released gates. Prove an outer promise stays pending while an active callback remains held after a peer fails. Add a timeout only as a safety limit for a broken test.
Your turn
Controlled Async Tests: practise
Create a deferred gate and a job that prints started, waits on it and then prints finished. Start the job, print held from the caller, release the gate and await completion. Expect started, held, finished.
// Keep a promise for the actual job and await it after releasing the gate.Hint 1: the idea
The job runs until its first await before returning control to the caller.
Hint 2: a first step
Save the resolver for a promise and call it only after printing held.
Show one solution
async function main() {
let release;
const gate = new Promise(resolve => {
release = resolve;
});
async function job() {
console.log("started");
await gate;
console.log("finished");
}
const work = job();
console.log("held");
release();
await work;
}
main();started
held
finishedExplain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?
Change it and test it
Release two gates in reverse order and assert the observed completion order. Then explain which signal a test should await to know that a third pool job started. A timeout may be a final safety net for a broken test, but should not create its intended ordering.
Completion is saved in this browser only. It does not measure mastery.