JavaScript
Jasmine
Testing
setTimeout
ErrorHandling

Jasmine test a setTimeout function throws an error

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Testing code that uses setTimeout in Jasmine requires controlling the clock because real timers are asynchronous and non-deterministic. Jasmine provides jasmine.clock() to mock setTimeout and setInterval, letting you advance time synchronously with clock.tick(). For testing that errors are thrown inside callbacks, you also need to handle the fact that errors inside setTimeout are not directly catchable by the test. This article covers the patterns for testing setTimeout behavior and error handling with Jasmine.

Basic Clock Mocking

javascript
1describe("setTimeout with Jasmine clock", () => {
2  beforeEach(() => {
3    jasmine.clock().install();
4  });
5
6  afterEach(() => {
7    jasmine.clock().uninstall();
8  });
9
10  it("should call the callback after the delay", () => {
11    const callback = jasmine.createSpy("callback");
12    setTimeout(callback, 1000);
13
14    expect(callback).not.toHaveBeenCalled();
15
16    jasmine.clock().tick(999);
17    expect(callback).not.toHaveBeenCalled();
18
19    jasmine.clock().tick(1);
20    expect(callback).toHaveBeenCalledTimes(1);
21  });
22});

jasmine.clock().install() replaces the native setTimeout with a mock. tick(ms) advances the mock clock and triggers any pending callbacks whose delay has elapsed.

Testing a Function That Uses setTimeout

javascript
1class Notifier {
2  constructor() {
3    this.messages = [];
4  }
5
6  scheduleNotification(message, delayMs) {
7    setTimeout(() => {
8      this.messages.push(message);
9    }, delayMs);
10  }
11}
12
13describe("Notifier", () => {
14  let notifier;
15
16  beforeEach(() => {
17    jasmine.clock().install();
18    notifier = new Notifier();
19  });
20
21  afterEach(() => {
22    jasmine.clock().uninstall();
23  });
24
25  it("should add message after the delay", () => {
26    notifier.scheduleNotification("Hello", 2000);
27
28    expect(notifier.messages.length).toBe(0);
29    jasmine.clock().tick(2000);
30    expect(notifier.messages).toEqual(["Hello"]);
31  });
32
33  it("should handle multiple scheduled notifications", () => {
34    notifier.scheduleNotification("First", 1000);
35    notifier.scheduleNotification("Second", 2000);
36
37    jasmine.clock().tick(1000);
38    expect(notifier.messages).toEqual(["First"]);
39
40    jasmine.clock().tick(1000);
41    expect(notifier.messages).toEqual(["First", "Second"]);
42  });
43});

Testing Error Throwing in setTimeout

Errors thrown inside setTimeout callbacks do not propagate to the caller. To test them, capture the error within the callback:

javascript
1function delayedValidation(value, delayMs) {
2  setTimeout(() => {
3    if (typeof value !== "string") {
4      throw new TypeError("Value must be a string");
5    }
6  }, delayMs);
7}
8
9describe("delayedValidation", () => {
10  beforeEach(() => {
11    jasmine.clock().install();
12  });
13
14  afterEach(() => {
15    jasmine.clock().uninstall();
16  });
17
18  it("should throw TypeError for non-string values", () => {
19    delayedValidation(42, 1000);
20
21    expect(() => {
22      jasmine.clock().tick(1000);
23    }).toThrowError(TypeError, "Value must be a string");
24  });
25
26  it("should not throw for string values", () => {
27    delayedValidation("hello", 1000);
28
29    expect(() => {
30      jasmine.clock().tick(1000);
31    }).not.toThrow();
32  });
33});

When Jasmine's mock clock executes the callback synchronously via tick(), any thrown error propagates to the tick() call, making it catchable with expect(...).toThrowError().

Refactoring for Testability

A better pattern is to use error callbacks or Promises instead of throwing inside setTimeout:

javascript
1function delayedAction(callback, delayMs) {
2  setTimeout(() => {
3    try {
4      const result = performWork();
5      callback(null, result);
6    } catch (error) {
7      callback(error);
8    }
9  }, delayMs);
10}
11
12describe("delayedAction with error callback", () => {
13  beforeEach(() => {
14    jasmine.clock().install();
15  });
16
17  afterEach(() => {
18    jasmine.clock().uninstall();
19  });
20
21  it("should pass error to callback on failure", () => {
22    const callback = jasmine.createSpy("callback");
23    delayedAction(callback, 500);
24
25    jasmine.clock().tick(500);
26    expect(callback).toHaveBeenCalledWith(jasmine.any(Error));
27  });
28});

Using Async/Await Instead of Clock

For modern code using Promises and setTimeout, test with async/await:

javascript
1function delay(ms) {
2  return new Promise((resolve) => setTimeout(resolve, ms));
3}
4
5async function fetchWithRetry(fetchFn, retries = 3) {
6  for (let i = 0; i < retries; i++) {
7    try {
8      return await fetchFn();
9    } catch (err) {
10      if (i === retries - 1) throw err;
11      await delay(1000 * (i + 1));
12    }
13  }
14}
15
16describe("fetchWithRetry", () => {
17  beforeEach(() => {
18    jasmine.clock().install();
19    jasmine.clock().mockDate();
20  });
21
22  afterEach(() => {
23    jasmine.clock().uninstall();
24  });
25
26  it("should retry on failure", async () => {
27    const fetchFn = jasmine
28      .createSpy("fetchFn")
29      .and.returnValues(
30        Promise.reject(new Error("fail")),
31        Promise.reject(new Error("fail")),
32        Promise.resolve("success")
33      );
34
35    const promise = fetchWithRetry(fetchFn);
36
37    jasmine.clock().tick(1000);
38    await Promise.resolve(); // Flush microtasks
39    jasmine.clock().tick(2000);
40    await Promise.resolve();
41
42    const result = await promise;
43    expect(result).toBe("success");
44    expect(fetchFn).toHaveBeenCalledTimes(3);
45  });
46});

Common Pitfalls

  • Forgetting to uninstall the clock: Not calling jasmine.clock().uninstall() in afterEach leaks the mock clock into subsequent tests, causing unpredictable failures. Always pair install() with uninstall().
  • Expecting errors to propagate from real setTimeout: Without Jasmine's mock clock, errors inside setTimeout callbacks go to the global error handler, not to the test. Use jasmine.clock().install() so that tick() executes callbacks synchronously and errors propagate to the caller.
  • Not ticking enough time: If the code schedules setTimeout(fn, 1000) and you only tick 999ms, the callback does not execute. Tick at least the full delay amount.
  • Testing implementation details instead of behavior: Verifying that setTimeout was called with specific arguments couples tests to implementation. Instead, test the observable effect (e.g., the callback was invoked, the state changed) after advancing the clock.
  • Mixing real and mock timers: Installing Jasmine's clock mocks all timer functions globally. If library code or other tests expect real timers, conflicts arise. Ensure the clock is only installed for tests that need it and uninstalled immediately after.

Summary

  • Use jasmine.clock().install() and tick(ms) to control setTimeout in tests
  • Always uninstall the clock in afterEach to prevent test pollution
  • Errors thrown inside setTimeout callbacks propagate through tick() when using mock clocks
  • Use expect(() => clock.tick(...)).toThrowError() to test errors in timer callbacks
  • Prefer error callbacks or Promises over throwing inside setTimeout for better testability
  • For modern async code, combine mock clocks with async/await and microtask flushing

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.