Freeze Time and Fast-Forward in Jest

The clock-control playbook I keep on a sticky note: freeze `Date.now`, advance pending timers without sleeping, and write tests for debounce/throttle helpers that actually finish in milliseconds.

JavaScript
Frontend
3 snippets
testing
unit-testing
code-template
rohanbakr

By @rohanbakr

May 2, 2026

·

Updated May 20, 2026

193 views

2

4.6 (8)

// The pattern: never call Date.now() or setTimeout directly inside the unit
// under test. Inject a clock and a setTimeout-like fn. The test owns time.

function makeClock(start = 0) {
    let now = start;
    const queue = []; // { fireAt, fn }
    return {
        now: () => now,
        setTimeout(fn, ms) {
            const fireAt = now + ms;
            const handle = { fireAt, fn };
            queue.push(handle);
            queue.sort((a, b) => a.fireAt - b.fireAt);
            return handle;
        },
        clearTimeout(handle) {
            const i = queue.indexOf(handle);
            if (i !== -1) queue.splice(i, 1);
        },
        advance(ms) {
            const target = now + ms;
            while (queue.length && queue[0].fireAt <= target) {
                const next = queue.shift();
                now = next.fireAt;
                next.fn();
            }
            now = target;
        },
    };
}

// Unit under test: a debounce that uses the injected clock.
function debounce(fn, wait, clock) {
    let timer = null;
    return function debounced(...args) {
        if (timer) clock.clearTimeout(timer);
        timer = clock.setTimeout(() => fn(...args), wait);
    };
}

const clock = makeClock();
const calls = [];
const debounced = debounce((x) => calls.push(x), 100, clock);
debounced('a');
debounced('b');
clock.advance(50);
console.log('after 50ms:', calls);   // []
debounced('c');                        // resets the timer
clock.advance(99);
console.log('after 99ms more:', calls); // []
clock.advance(1);
console.log('after 100ms total since c:', calls); // ['c']

The fundamental move is dependency injection: the unit under test receives a clock object instead of reaching for the global Date.now and setTimeout. In production you wire it to globalThis; in tests you wire it to a fake whose advance method walks the queue deterministically. This is exactly what jest.useFakeTimers() does under the hood, but writing a 30-line clock once teaches you why the flaky tests happen: any code that reads time without going through the injected clock will desync from the test. The test for our debounce now finishes in microseconds and never relies on setTimeout(0) tricks.