Skip to the code
cf.

Promise with a timeout

Write withTimeout(promise, ms). It returns a new promise. If promise settles within ms milliseconds, the new promise settles the same way: it fulfills with the same value or rejects with the same reason.

If ms pass first, it rejects with an Error whose name is "TimeoutError" (the message is up to you). Once promise has settled, clear the timer, so nothing is left scheduled.

try {
  const user = await withTimeout(fetchJson("/api/user"), 2000);
  showUser(user);
} catch (error) {
  if (error.name === "TimeoutError") showMessage("The server is taking too long.");
  else throw error;
}

What the tests check

  1. fulfills with the value when the promise is in time
    const clock = useFakeTimers();
    const data = new Promise((resolve) => setTimeout(resolve, 100, "data"));
    let result;
    withTimeout(data, 200).then((value) => {
      result = value;
    });
    await clock.tickAsync(100);
    expect(result).toBe("data");
    
  2. rejects with the promise's reason when it rejects in time
    const clock = useFakeTimers();
    const error = new Error("server error");
    const failing = new Promise((_, reject) => setTimeout(reject, 50, error));
    failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
    let reason;
    withTimeout(failing, 100).catch((e) => {
      reason = e;
    });
    await clock.tickAsync(50);
    expect(reason).toBe(error);
    
  3. rejects once ms pass without the promise settling
    const clock = useFakeTimers();
    let reason;
    withTimeout(new Promise(() => {}), 100).catch((e) => {
      reason = e;
    });
    await clock.tickAsync(99);
    expect(reason).toBeUndefined();
    await clock.tickAsync(1);
    expect(reason).toBeDefined();
    
  4. the timeout error is an Error named TimeoutError
    const clock = useFakeTimers();
    let reason;
    withTimeout(new Promise(() => {}), 100).catch((e) => {
      reason = e;
    });
    await clock.tickAsync(100);
    expect(reason).toBeInstanceOf(Error);
    expect(reason.name).toBe("TimeoutError");
    
  5. clears its timer once the promise fulfills
    const clock = useFakeTimers();
    withTimeout(Promise.resolve("fast"), 1000);
    await flush();
    expect(clock.pending()).toBe(0);
    
  6. clears its timer once the promise rejects
    const clock = useFakeTimers();
    const failing = Promise.reject(new Error("fast failure"));
    failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
    withTimeout(failing, 1000).catch(() => {}); // it rejects too; this test only checks the timer
    await flush();
    expect(clock.pending()).toBe(0);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).