sleep function
Waits for a delay, cancellable with an AbortSignal that clears the timer.
ts
import { sleep } from 'utils';Signature
ts
export async function sleep(ms = 0, signal?: AbortSignal): Promise<void>Parameters
| Name | Description |
|---|---|
ms | The delay in milliseconds. Defaults to 0. |
signal | The signal that cancels the wait. |
Returns
A promise resolved once the delay has elapsed.
Errors
- Rejects Error: With the signal's reason when
signalis aborted.
Example
ts
await sleep(100);
await sleep(1000, controller.signal); // rejects as soon as controller.abort() is calledTests
Line coverage: 100 %.
- ✅ resolves after the delay
- ✅ rejects when the signal is aborted, and clears the timer
- ✅ rejects at once when the signal is already aborted
- ✅ wraps a non-error abort reason
- ✅ removes its abort listener once resolved
Source
src/async/sleep.ts (on GitHub)
Source code
ts
function abortReason(signal: AbortSignal): Error {
const reason: unknown = signal.reason;
return reason instanceof Error ? reason : new Error(String(reason));
}
/**
* Waits for a delay, cancellable with an `AbortSignal` that clears the timer.
*
* @param ms - The delay in milliseconds. Defaults to `0`.
* @param signal - The signal that cancels the wait.
* @returns A promise resolved once the delay has elapsed.
* @rejects {Error} With the signal's reason when `signal` is aborted.
* @example
* await sleep(100);
* await sleep(1000, controller.signal); // rejects as soon as controller.abort() is called
*/
export async function sleep(ms = 0, signal?: AbortSignal): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (signal?.aborted === true) {
reject(abortReason(signal));
return;
}
const timer = setTimeout(() => {
signal?.removeEventListener('abort', onAbort);
resolve();
}, ms);
function onAbort(this: AbortSignal): void {
clearTimeout(timer);
reject(abortReason(this));
}
signal?.addEventListener('abort', onAbort, { once: true });
});
}Test file
ts
import { sleep } from './sleep';
describe(sleep, () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('resolves after the delay', async () => {
const onResolved = vi.fn<() => void>();
async function sleepThenNotify(): Promise<void> {
await sleep(100);
onResolved();
}
const promise = sleepThenNotify();
await vi.advanceTimersByTimeAsync(99);
expect(onResolved).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
await promise;
expect(onResolved).toHaveBeenCalledOnce();
});
it('rejects when the signal is aborted, and clears the timer', async () => {
const controller = new AbortController();
const promise = sleep(100, controller.signal);
controller.abort();
await expect(promise).rejects.toThrow(expect.objectContaining({ name: 'AbortError' }));
expect(vi.getTimerCount()).toBe(0);
});
it('rejects at once when the signal is already aborted', async () => {
await expect(sleep(100, AbortSignal.abort(new Error('Stopped')))).rejects.toThrow('Stopped');
expect(vi.getTimerCount()).toBe(0);
});
it('wraps a non-error abort reason', async () => {
const controller = new AbortController();
const promise = sleep(100, controller.signal);
controller.abort('cancelled');
await expect(promise).rejects.toThrow('cancelled');
});
it('removes its abort listener once resolved', async () => {
const controller = new AbortController();
const removeListener = vi.spyOn(controller.signal, 'removeEventListener');
const promise = sleep(10, controller.signal);
await vi.advanceTimersByTimeAsync(10);
await promise;
expect(removeListener).toHaveBeenCalledOnce();
});
});Keywords: sleep, async, function