diff --git a/README.md b/README.md
index 050ceb9c3..711aa7a6f 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,7 @@ Next generation testing framework powered by Vite.
- Out-of-box TypeScript / JSX support
- Filtering, timeouts, concurrent for suite and tests
- Sharding support
+- Reporting Uncaught Errors
- Run your tests in the browser natively (experimental)
> Vitest requires Vite >=v5.0.0 and Node >=v18.0.0
diff --git a/docs/.vitepress/components/FeaturesList.vue b/docs/.vitepress/components/FeaturesList.vue
index d77ef6232..a82fdd4f1 100644
--- a/docs/.vitepress/components/FeaturesList.vue
+++ b/docs/.vitepress/components/FeaturesList.vue
@@ -26,7 +26,8 @@
Code coverage via v8 or istanbul
Rust-like in-source testing
Type Testing via expect-type
- Sharding support
+ Sharding Support
+ Reporting Uncaught Errors
diff --git a/docs/guide/features.md b/docs/guide/features.md
index 38b02e873..0a5312d88 100644
--- a/docs/guide/features.md
+++ b/docs/guide/features.md
@@ -259,3 +259,60 @@ export default defineConfig(({ mode }) => ({
},
}))
```
+
+## Unhandled Errors
+
+By default, Vitest catches and reports all [unhandled rejections](https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event), [uncaught exceptions](https://nodejs.org/api/process.html#event-uncaughtexception) (in Node.js) and [error](https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event) events (in the [browser](/guide/browser/)).
+
+You can disable this behaviour by catching them manually. Vitest assumes the callback is handled by you and won't report the error.
+
+::: code-group
+```ts [setup.node.js]
+// in Node.js
+process.on('unhandledRejection', () => {
+ // your own handler
+})
+
+process.on('uncaughtException', () => {
+ // your own handler
+})
+```
+```ts [setup.browser.js]
+// in the browser
+window.addEventListener('error', () => {
+ // your own handler
+})
+
+window.addEventListener('unhandledrejection', () => {
+ // your own handler
+})
+```
+:::
+
+Alternatively, you can also ignore reported errors with a [`dangerouslyIgnoreUnhandledErrors`](/config/#dangerouslyignoreunhandlederrors) option. Vitest will still report them, but they won't affect the test result (exit code won't be changed).
+
+If you need to test that error was not caught, you can create a test that looks like this:
+
+```ts
+test('my function throws uncaught error', async ({ onTestFinished }) => {
+ onTestFinished(() => {
+ // if the event was never called during the test,
+ // make sure it's removed before the next test starts
+ process.removeAllListeners('unhandledrejection')
+ })
+
+ return new Promise((resolve, reject) => {
+ process.once('unhandledrejection', (error) => {
+ try {
+ expect(error.message).toBe('my error')
+ resolve()
+ }
+ catch (error) {
+ reject(error)
+ }
+ })
+
+ callMyFunctionThatRejectsError()
+ })
+})
+```
diff --git a/packages/vitest/src/runtime/execute.ts b/packages/vitest/src/runtime/execute.ts
index 96497b162..c3afd590c 100644
--- a/packages/vitest/src/runtime/execute.ts
+++ b/packages/vitest/src/runtime/execute.ts
@@ -58,14 +58,11 @@ function listenForErrors(state: () => WorkerGlobalState) {
function catchError(err: unknown, type: string, event: 'uncaughtException' | 'unhandledRejection') {
const worker = state()
- // if error happens during a test
- if (worker.current?.type === 'test') {
- const listeners = process.listeners(event as 'uncaughtException')
- // if there is another listener, assume that it's handled by user code
- // one is Vitest's own listener
- if (listeners.length > 1) {
- return
- }
+ const listeners = process.listeners(event as 'uncaughtException')
+ // if there is another listener, assume that it's handled by user code
+ // one is Vitest's own listener
+ if (listeners.length > 1) {
+ return
}
const error = processError(err)
diff --git a/test/cli/fixtures/fails/unhandled-suite.test.ts b/test/cli/fixtures/fails/unhandled-suite.test.ts
new file mode 100644
index 000000000..d73e61571
--- /dev/null
+++ b/test/cli/fixtures/fails/unhandled-suite.test.ts
@@ -0,0 +1,8 @@
+import { expect, it } from 'vitest';
+
+it('foo', () => {
+ expect(1).toBe(1)
+ new Promise((resolve, reject) => {
+ reject('promise error');
+ });
+});
diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap
index d784425a9..78cd736b7 100644
--- a/test/cli/test/__snapshots__/fails.test.ts.snap
+++ b/test/cli/test/__snapshots__/fails.test.ts.snap
@@ -127,3 +127,5 @@ exports[`should fail unhandled.test.ts 1`] = `
"Error: some error
Error: Uncaught [Error: some error]"
`;
+
+exports[`should fail unhandled-suite.test.ts 1`] = `"Unknown Error: promise error"`;
diff --git a/test/core/test/unhandled.test.ts b/test/core/test/unhandled.test.ts
new file mode 100644
index 000000000..6dca9961c
--- /dev/null
+++ b/test/core/test/unhandled.test.ts
@@ -0,0 +1,12 @@
+import { test } from 'vitest'
+
+process.on('unhandledRejection', () => {
+ // ignore errors
+})
+
+test('throws unhandled but not reported', () => {
+ // eslint-disable-next-line no-new
+ new Promise((resolve, reject) => {
+ reject(new Error('promise error'))
+ })
+})