Build retry(work, attempts, signal) independently. Attempts is a positive integer. Work is called sequentially with a one-based attempt number and the signal; never start overlapping attempts. Return on first success, retry a rejected attempt while attempts remain, and preserve the final thrown value.
Check cancellation before every attempt. If the signal becomes aborted while work is failing, reject with its reason instead of starting a retry. Cancellation remains cooperative inside work. The helper has no backoff or timeout; those would need additional policy and a clock dependency.
Test success on the second attempt, exhaustion, pre-abort, abort during a failed attempt, and invalid attempts. Include undefined as the final failure. The worked answer shows these contracts in a compact form; try the task before reading it.
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() {
let calls = 0;
async function work() {
calls++;
if (calls < 2) {
throw new Error("temporary");
}
return "ok";
}
try {
await work();
} catch {
console.log("retry needed");
}
console.log(await work());
}
main();Check your prediction
retry needed
ok Your turn
On Your Own: Sequential Retry: practise
Implement retry and show success on attempt 2 as ok 2. Also assert that a pre-aborted signal starts no work and that exhausting a callback that throws undefined still rejects. Print edge cases passed after those checks.
// Validate once, then check abort before each call and after any failure.Hint 1: the idea
A success exits immediately. A failed final attempt must still throw, even if its value is undefined.
Hint 2: a first step
Use the loop index to decide whether a rejection can be retried.
Show one solution
async function main() {
async function retry(work, attempts, signal) {
if (!Number.isInteger(attempts) || attempts < 1) {
throw new RangeError("Invalid attempts");
}
for (let attempt = 1; attempt <= attempts; attempt++) {
signal?.throwIfAborted();
try {
return await work(attempt, signal);
} catch (error) {
signal?.throwIfAborted();
if (attempt === attempts) {
throw error;
}
}
}
}
let calls = 0;
const result = await retry(async () => {
calls++;
if (calls < 2) {
throw "temporary";
}
return "ok";
}, 3);
console.log(result, calls);
const controller = new AbortController();
controller.abort("cancelled");
let started = 0;
let cancelled = false;
let exhausted = false;
try {
await retry(
() => {
started++;
},
2,
controller.signal
);
} catch (error) {
cancelled = error === "cancelled";
}
try {
await retry(() => {
throw undefined;
}, 2);
} catch (error) {
exhausted = error === undefined;
}
let invalid = false;
let cancelledDuring = false;
let duringCalls = 0;
try {
await retry(() => 1, 0);
} catch (error) {
invalid = error instanceof RangeError;
}
const during = new AbortController();
try {
await retry(
() => {
duringCalls++;
during.abort("during");
throw "temporary";
},
3,
during.signal
);
} catch (error) {
cancelledDuring = error === "during";
}
if (
!cancelled ||
started !== 0 ||
!exhausted ||
!invalid ||
!cancelledDuring ||
duringCalls !== 1
) {
throw new Error("Edge contract failed");
}
console.log("edge cases passed");
}
main();ok 2
edge cases passedExplain 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
Review the assertions for abort during a failed attempt and invalid attempts, then add a direct first-attempt-success case. Prove calls do not overlap with an active counter. Temporarily remove the post-failure abort check and confirm your cancellation test fails. Consider idempotency before applying retries to real side-effecting work.
Completion is saved in this browser only. It does not measure mastery.