Short answer
In Vitest 3.x the testTimeout is measured against the real wall‑clock, even when a test has enabled fake timers. If a test never advances its fake timers, the worker will abort the test after the real‑time timeout has elapsed. There is no built‑in cooperative cancellation signal that propagates into the test body on timeout or SIGINT; each test simply fails when its timeout is hit.
What changed from 2.x to 3.x?
- 2.x behavior:
testTimeout was effectively paused until fake timers were advanced. A test that never called vi.advanceTimersByTime() could run forever without triggering a timeout. - 3.x behavior: The timeout is now applied to the real clock. If fake timers are not progressed, the worker aborts the test after the configured timeout. This change was introduced in the 3.0.0‑beta releases to prevent silent hangs in concurrent workers.
Concurrent mode specifics
Vitest runs each test file in its own worker. Each worker has an isolated fake‑timer context, so:
- Timeouts are enforced per test, not per worker.
- When a test uses fake timers and never advances them, the worker will abort that test after the real‑time
testTimeout. The abort is local to the test; it does not cancel other tests in the same or different workers. - There is no global abort signal that flows into the test code on
SIGINT or on timeout. The test simply receives a failure event.
Reproducing the change
- Create
timeout.test.ts:
import { test } from 'vitest';
// 500 ms real‑time timeout
const timeout = 500;
// Enable fake timers for this test only
vi.useFakeTimers();
test('fails after real‑time timeout', async () => {
// Do not advance timers – the test will hang until the real timeout
await new Promise(r => setTimeout(r, 1000));
}, timeout);
- Run Vitest with concurrent mode (the default):
vitest run. The test should fail after ~500 ms real time, even though the fake timer was never advanced. - Run the same test with
--runInBand to see that the behavior is identical – the timeout is still enforced on the real clock. - Now add a timer advance:
vi.advanceTimersByTime(1000); // or vi.runAllTimers();
- Re‑run the test. It should pass because the fake timers are advanced before the async operation resolves, and the real‑time timeout is no longer reached.
Practical implications
- If you enable fake timers globally (e.g., in
setup.ts), every test will be subject to this real‑time timeout rule. Tests that rely on real timers may still hit the timeout, which can be confusing. - When mixing real and fake timers in the same test file, remember that the timeout is applied to the real clock regardless of which timers are in use.
- Because Vitest does not emit a cancellation signal into the test body, you cannot rely on
AbortController or similar to detect a timeout from within the test. Instead, design your async code to be idempotent or to handle a thrown TimeoutError if you need to react programmatically.
Missing diagnostic detail
To give a more precise recommendation, could you confirm whether you enable fake timers globally (in a setup file) or only within individual tests? Global fake timers can mask timeouts for unrelated tests, especially in concurrent mode.