Library tests need collaborators that misbehave in specific ways. A mapper may throw synchronously before returning a promise, return a rejected promise, ignore cancellation temporarily, or reject with undefined. A source iterator may throw from next. Its return cleanup may also throw.
A useful test identifies the public outcome and the work that must not happen afterward. The task pool wraps mapper failure with index and cause, preserves source errors, preserves external abort reasons, and does not let a cleanup failure replace an existing primary failure.
Be careful with try/catch tests: throwing an assertion inside the same try block can make a broken operation appear to have thrown correctly. Keep the assertion after the catch, or use a dedicated assertion helper. In the example, didThrow reports whether the operation failed, including when the thrown value is undefined.
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 didThrow(work) {
try {
work();
return false;
} catch {
return true;
}
}
console.log(
didThrow(() => {
throw undefined;
})
);
console.log(didThrow(() => 1));
}
main();Check your prediction
true
false Distinguish call-time throws from promise rejection
An async boundary can fail while invoking a collaborator or while awaiting what it returned. Keep both operations inside the intended try/catch. A test that only returns a rejected promise misses a synchronous throw before any promise is produced.
Cleanup can also fail. Decide whether a secondary cleanup error replaces the primary outcome; this pool preserves an existing primary failure. Test identity, not merely a similar error message, to demonstrate that preservation.
async function main() {
async function outcome(work) {
try {
await work();
return "ok";
} catch {
return "failed";
}
}
console.log(
await outcome(() => {
throw "sync";
})
);
console.log(await outcome(() => Promise.reject("async")));
console.log(await outcome(() => Promise.resolve(0)));
}
main();Check your prediction
failed
failed
ok The same adapter sees both failure paths and preserves a fulfilled zero as success. Removing await would lose the asynchronous rejection path.
const primary = new Error("mapper");
const cleanup = new Error("cleanup");
let failure = primary;
try {
throw cleanup;
} catch (error) {
if (failure === undefined) {
failure = error;
}
}
console.log(failure === primary);Check your prediction
true This isolated example begins with an actual primary Error. In a general implementation, a separate failed flag is needed because undefined can itself be the primary thrown reason.
Try the rule on new inputs
Build fixtures for synchronous mapper throw, asynchronous rejection, iterator.next throw and iterator.return throw. State which exact error reaches the caller in each case, including when both primary and cleanup fail.
Your turn
Failure-Path Tests: practise
Write rejects(work), an async boolean helper that awaits work and returns true if it rejects. Test a synchronous throw, a rejected promise and a resolved promise; print true, true, false.
// Put both the call and its await inside try so synchronous and asynchronous failures are covered.Hint 1: the idea
A callback can fail before it returns its promise.
Hint 2: a first step
Return false only after the awaited operation succeeds.
Show one solution
async function main() {
async function rejects(work) {
try {
await work();
return false;
} catch {
return true;
}
}
console.log(
await rejects(() => {
throw undefined;
})
);
console.log(await rejects(() => Promise.reject("bad")));
console.log(await rejects(() => Promise.resolve(1)));
}
main();true
true
falseExplain 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
To investigate a missing await safely, first store const failed = Promise.reject("bad") and attach failed.catch(() => {}) to handle its rejection. Pass () => failed to a temporary copy of rejects without await: it incorrectly returns false. The original helper must return true. Handling the fixture separately prevents an unhandled rejection during this experiment. For the pool, identify separate tests for source next failure, mapper failure, external abort and cleanup failure.
Completion is saved in this browser only. It does not measure mastery.