Eventually Assertion Helper
Poll an async predicate until it succeeds or times out, with abort support and the last error preserved as the cause.
Code
export interface EventuallyOptions {
timeoutMs?: number;
intervalMs?: number;
signal?: AbortSignal;
}
export async function eventually<T>(
predicate: () => T | Promise<T>,
options: EventuallyOptions = {},
): Promise<T> {
const { timeoutMs = 2000, intervalMs = 50, signal } = options;
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
throw new RangeError("timeoutMs must be a non-negative finite number");
}
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
throw new RangeError("intervalMs must be a positive finite number");
}
const deadline = Date.now() + timeoutMs;
let lastError: unknown;
while (true) {
if (signal?.aborted) {
const aborted = new Error("eventually aborted");
aborted.name = "AbortError";
throw aborted;
}
try {
const value = await predicate();
if (value) return value;
} catch (error) {
lastError = error;
}
if (Date.now() >= deadline) {
const message = "eventually timed out after " + timeoutMs + "ms";
throw lastError ? new Error(message, { cause: lastError }) : new Error(message);
}
const wait = Math.min(intervalMs, Math.max(0, deadline - Date.now()));
await new Promise<void>((resolve) => setTimeout(resolve, wait));
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.