Detect hanging async operations in Vitest

Last updated: 19.02.2026

Context:

  • Environment: Any TypeScript/JavaScript project using Vitest
  • Dependencies:
    • Vitest version: "vitest": "4.1.0" currently its beta, but it will be stable very soon.

Problem:

You have a test that starts an async operation - a setInterval, a web stream, an event listener - but never cleans it up.

The test passes, but that operation keeps running, leaking into subsequent tests and causing random failures in CI that are nearly impossible to reproduce locally.

A MessageChannel opened in a test but never closed. Ports keep listening, leaking into subsequent tests.

export function createChannel() {
  const { port1, port2 } = new MessageChannel()
  return {
    send: (msg: string) => port1.postMessage(msg),
    onMessage: (fn: (msg: string) => void) => {
      port2.onmessage = ({ data }) => fn(data)
    },
    close: () => {
      port1.close()
      port2.close()
    }
  }
}
describe('createChannel', () => {
  it('should receive sent message', async () => {
    const channel = createChannel()
    const fn = vi.fn()

    channel.onMessage(fn)
    channel.send('hello')

    await vi.waitFor(() => expect(fn).toHaveBeenCalledWith('hello'))
    // channel.close() never called - ports leak
  })
})

Solution:

Enable detectAsyncLeaks in your config for debugging:

// vitest.config.*
export default defineConfig({
  test: {
    detectAsyncLeaks: true,
  },
})

Now Vitest will pinpoint the leak:

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Async Leaks 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
MESSAGEPORT leaking in src/createChannel.spec.ts
  1| export function createChannel() {
  2|   const { port1, port2 } = new MessageChannel()
   |                            ^
  3|   return {
  4|     send: (msg: string) => port1.postMessage(msg),
 ❯ createChannel src/createChannel.ts:2:28
 ❯ src/createChannel.spec.ts:6:21

Fix:

describe('createChannel', () => {
  it('should receive sent message', async () => {
    const channel = createChannel()
    const fn = vi.fn()

    channel.onMessage(fn)
    channel.send('hello')

    await vi.waitFor(() => expect(fn).toHaveBeenCalledWith('hello'))
    channel.close() // proper cleanup
  })
})

Performance note: detectAsyncLeaks slows down test execution noticeably. Use it for debugging or in a dedicated CI job - not as default in your main pipeline.

Further reading:

Official Docs

Pull request