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.
- Vitest version:
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: