Skip to content
Latchkey

Vitest Unhandled Rejection Fails the Suite in CI

Vitest failed the run because a promise rejected outside any awaited test - an unhandled rejection. The work was fired-and-forgotten, so its rejection surfaced after the test that started it, failing the whole file.

What this error means

The run reports "Unhandled Rejection" (or "Vitest caught N unhandled errors during the test run") even though individual tests show as passed. The stack often points at a setTimeout, a floating promise, or teardown.

Vitest output
Unhandled Rejection
Error: connect ECONNREFUSED 127.0.0.1:5432
  ❯ Timeout._onTimeout src/poller.ts:14:11

This error originated in "src/poller.test.ts". It was caught after the
test had already finished.

Common causes

A floating (unawaited) promise rejected

A test started async work without awaiting it. The test ended green, then the promise rejected with no handler, so Vitest flags an unhandled rejection.

Background timer/poller not stopped

A setInterval/poller created in a test keeps running after teardown and eventually rejects (e.g. a closed connection), failing a later or already-finished test.

How to fix it

Await all async work

Return or await every promise so its rejection is attached to the test, not the global handler.

poller.test.ts
it('polls once', async () => {
  await expect(pollOnce()).resolves.toBeDefined();
});

Stop background work in teardown

poller.test.ts
let timer;
beforeEach(() => { timer = startPoller(); });
afterEach(() => { clearInterval(timer); });

How to prevent it

  • Never fire-and-forget promises in tests; await them.
  • Clear timers/intervals and close connections in afterEach.
  • Use fake timers to make time-based code deterministic.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →