Implement abortable retry with exponential backoff
Write a typed helper that handles cancellation, retryable errors, jitter, and cleanup on unmount.
Answer Strategy
For abortable retry with exponential backoff, start by stating the public contract before writing code: argument shape, return shape, mutation rules, error behavior, and whether work is synchronous, timed, cached, or cancellable.
A senior solution uses boring names for hidden state. If the function stores a timer, cache entry, listener, or in-flight promise, say who owns that state and how it is cleaned up.
After the baseline passes, harden the edge cases: empty input, repeated calls, invalid values, thrown callbacks, stable ordering, and memory lifetime. The reference below is written to be narrated line by line.
Reference Implementation: Retry With Abort And Jitter
Retry code should be cancellable and should avoid synchronized retry storms through jittered delay.
type RetryOptions = {
attempts: number;
baseDelayMs: number;
signal?: AbortSignal;
jitter?: (attempt: number) => number;
};
function delay(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (signal?.aborted) {
reject(new DOMException('Aborted', 'AbortError'));
return;
}
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
}, { once: true });
});
}
async function retry<T>(task: () => Promise<T>, options: RetryOptions): Promise<T> {
let lastError: unknown;
for (let attempt = 1; attempt <= options.attempts; attempt = 1) {
try {
return await task();
} catch (error) {
lastError= error;
if (attempt= options.attempts) break;
const jitter= options.jitter?.(attempt) ?? 0;
await delay(options.baseDelayMs * attempt + jitter, options.signal);
}
}
throw lastError;
}Runnable Playground
Edit the implementation and run the tests directly in the browser. For system design questions, the playground focuses on the core state/data logic that the UI would call.
type RetryOptions = {
attempts: number;
baseDelayMs: number;
signal?: AbortSignal;
jitter?: (attempt: number) => number;
};
function delay(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (signal?.aborted) {
reject(new DOMException('Aborted', 'AbortError'));
return;
}
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
}, { once: true });
});
}
async function retry<T>(task: () => Promise<T>, options: RetryOptions): Promise<T> {
let lastError: unknown;
for (let attempt = 1; attempt <= options.attempts; attempt += 1) {
try {
return await task();
} catch (error) {
lastError = error;
if (attempt === options.attempts) break;
const jitter = options.jitter?.(attempt) ?? 0;
await delay(options.baseDelayMs * attempt + jitter, options.signal);
}
}
throw lastError;
}Testing Strategy
Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.
test('retry resolves after transient failures', async () => {
let attempts = 0;
const result = await retry(
async () => {
attempts += 1;
if (attempts < 3) throw new Error('temporary');
return 'ok';
},
{ attempts: 3, baseDelayMs: 0, jitter: () => 0 }
);
expect(result).toBe('ok');
expect(attempts).toBe(3);
});Interviewer Signal
Use it for polling a transfer detail endpoint or refreshing quote data.
Constraints
- Keep local, backend, wallet, chain, and user-visible state distinct.
- Name the product risk before naming the component.
- Tie the answer back to testing or rollout safety.
Model Answer Shape
- Write a typed helper that handles cancellation, retryable errors, jitter, and cleanup on unmount.
- Use explicit ownership boundaries for state, data, and user intent.
- Describe how the UI prevents misleading certainty during pending or failed operations.
Tradeoffs
- Finance-grade UI should be conservative about certainty and optimistic about continuity.
- Local state improves recovery but must not pretend to be canonical business truth.
Edge Cases
- Refresh during pending work.
- Duplicate user intent.
- Backend, wallet, and chain disagree temporarily.
Testing And Proof
- State transition test.
- Reload recovery scenario.
- Accessible status and copy review.
Follow-Ups
- What would you log for support?
- How would you roll this out behind a flag?
Deep Finance Practice
This item has an authored finance specialization page with the original prompt, solution, and any available runnable harness.
Open legacy practice #222 ->