From 464218f42cb4812f4dba77674f2152606c4c215f Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 13 May 2025 19:28:56 +0200 Subject: [PATCH] fix: try to catch unhandled error outside of a test (#7968) --- README.md | 1 + docs/.vitepress/components/FeaturesList.vue | 3 +- docs/guide/features.md | 57 +++++++++++++++++++ packages/vitest/src/runtime/execute.ts | 13 ++--- .../fixtures/fails/unhandled-suite.test.ts | 8 +++ .../cli/test/__snapshots__/fails.test.ts.snap | 2 + test/core/test/unhandled.test.ts | 12 ++++ 7 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 test/cli/fixtures/fails/unhandled-suite.test.ts create mode 100644 test/core/test/unhandled.test.ts 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')) + }) +}) -- 2.51.2