diff --git a/docs/config/index.md b/docs/config/index.md
index 88d11a270..3bd4e9627 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -2191,6 +2191,30 @@ export default defineConfig({
})
```
+### onUnhandledError {#onunhandlederror}
+
+- **Type:** `(error: (TestError | Error) & { type: string }) => boolean | void`
+
+A custom handler to filter out unhandled errors that should not be reported. If an error is filtered out, it will no longer affect the test results.
+
+If you want unhandled errors to be reported without impacting the test outcome, consider using the [`dangerouslyIgnoreUnhandledErrors`](#dangerouslyIgnoreUnhandledErrors) option
+
+```ts
+import type { ParsedStack } from 'vitest'
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ onUnhandledError(error): boolean | void {
+ // Ignore all errors with the name "MySpecialError".
+ if (error.name === 'MySpecialError') {
+ return false
+ }
+ },
+ },
+})
+```
+
### diff
- **Type:** `string`
diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts
index afc9a8a1f..33790a0bf 100644
--- a/packages/vitest/src/node/core.ts
+++ b/packages/vitest/src/node/core.ts
@@ -221,7 +221,9 @@ export class Vitest {
const resolved = resolveConfig(this, options, server.config)
this._config = resolved
- this._state = new StateManager()
+ this._state = new StateManager({
+ onUnhandledError: resolved.onUnhandledError,
+ })
this._cache = new VitestCache(this.version)
this._snapshot = new SnapshotManager({ ...resolved.snapshotOptions })
this._testRun = new TestRun(this)
diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts
index 6121cc682..ba1be9c1e 100644
--- a/packages/vitest/src/node/state.ts
+++ b/packages/vitest/src/node/state.ts
@@ -2,6 +2,7 @@ import type { File, Task, TaskResultPack } from '@vitest/runner'
import type { UserConsoleLog } from '../types/general'
import type { TestProject } from './project'
import type { MergedBlobs } from './reporters/blob'
+import type { OnUnhandledErrorCallback } from './types/config'
import { createFileTask } from '@vitest/runner/utils'
import { TestCase, TestModule, TestSuite } from './reporters/reported-tasks'
@@ -23,31 +24,43 @@ export class StateManager {
reportedTasksMap: WeakMap = new WeakMap()
blobs?: MergedBlobs
- catchError(err: unknown, type: string): void {
- if (isAggregateError(err)) {
- return err.errors.forEach(error => this.catchError(error, type))
+ onUnhandledError?: OnUnhandledErrorCallback
+
+ constructor(
+ options: {
+ onUnhandledError?: OnUnhandledErrorCallback
+ },
+ ) {
+ this.onUnhandledError = options.onUnhandledError
+ }
+
+ catchError(error: unknown, type: string): void {
+ if (isAggregateError(error)) {
+ return error.errors.forEach(error => this.catchError(error, type))
}
- if (err === Object(err)) {
- (err as Record).type = type
+ if (typeof error === 'object' && error !== null) {
+ (error as Record).type = type
}
else {
- err = { type, message: err }
+ error = { type, message: error }
}
- const _err = err as Record
- if (_err && typeof _err === 'object' && _err.code === 'VITEST_PENDING') {
- const task = this.idMap.get(_err.taskId)
+ const _error = error as Record
+ if (_error && typeof _error === 'object' && _error.code === 'VITEST_PENDING') {
+ const task = this.idMap.get(_error.taskId)
if (task) {
task.mode = 'skip'
task.result ??= { state: 'skip' }
task.result.state = 'skip'
- task.result.note = _err.note
+ task.result.note = _error.note
}
return
}
- this.errorsSet.add(err)
+ if (!this.onUnhandledError || this.onUnhandledError(error as any) !== false) {
+ this.errorsSet.add(error)
+ }
}
clearErrors(): void {
diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts
index 3f70e6e00..7b74dc544 100644
--- a/packages/vitest/src/node/types/config.ts
+++ b/packages/vitest/src/node/types/config.ts
@@ -638,6 +638,11 @@ export interface InlineConfig {
*/
onStackTrace?: (error: TestError, frame: ParsedStack) => boolean | void
+ /**
+ * A callback that can return `false` to ignore an unhandled error
+ */
+ onUnhandledError?: OnUnhandledErrorCallback
+
/**
* Indicates if CSS files should be processed.
*
@@ -978,6 +983,8 @@ export interface UserConfig extends InlineConfig {
mergeReports?: string
}
+export type OnUnhandledErrorCallback = (error: (TestError | Error) & { type: string }) => boolean | void
+
export interface ResolvedConfig
extends Omit<
Required,
diff --git a/test/browser/specs/unhandled.test.ts b/test/browser/specs/unhandled.test.ts
index 009096da8..97ed5d109 100644
--- a/test/browser/specs/unhandled.test.ts
+++ b/test/browser/specs/unhandled.test.ts
@@ -14,3 +14,16 @@ test('prints correct unhandled error stack', async () => {
expect(stderr).toContain('throw-unhandled-error.test.ts:9:20')
}
})
+
+test('ignores unhandled errors', async () => {
+ const { stderr } = await runBrowserTests({
+ root: './fixtures/unhandled',
+ onUnhandledError(error) {
+ if (error.message.includes('custom_unhandled_error')) {
+ return false
+ }
+ },
+ })
+
+ expect(stderr).toBe('')
+})
diff --git a/test/browser/specs/utils.ts b/test/browser/specs/utils.ts
index aa47f8898..54f34597f 100644
--- a/test/browser/specs/utils.ts
+++ b/test/browser/specs/utils.ts
@@ -1,5 +1,5 @@
import type { UserConfig as ViteUserConfig } from 'vite'
-import type { UserConfig } from 'vitest/node'
+import type { TestUserConfig } from 'vitest/node'
import type { VitestRunnerCLIOptions } from '../../test-utils'
import { runVitest } from '../../test-utils'
import { browser } from '../settings'
@@ -7,7 +7,7 @@ import { browser } from '../settings'
export { browser, instances, provider } from '../settings'
export async function runBrowserTests(
- config?: Omit & { browser?: Partial },
+ config?: Omit & { browser?: Partial },
include?: string[],
viteOverrides?: Partial,
runnerOptions?: VitestRunnerCLIOptions,
@@ -19,6 +19,6 @@ export async function runBrowserTests(
browser: {
headless: browser !== 'safari',
...config?.browser,
- } as UserConfig['browser'],
+ } as TestUserConfig['browser'],
}, include, 'test', viteOverrides, runnerOptions)
}
diff --git a/test/cli/test/unhandled-ignore.test.ts b/test/cli/test/unhandled-ignore.test.ts
new file mode 100644
index 000000000..b99439f86
--- /dev/null
+++ b/test/cli/test/unhandled-ignore.test.ts
@@ -0,0 +1,21 @@
+import { expect, test } from 'vitest'
+import { runVitest } from '../../test-utils'
+
+test('run mode does not get stuck when TTY', async () => {
+ const { vitest } = await runVitest({
+ root: './fixtures/fails',
+ include: ['unhandled.test.ts'],
+ onUnhandledError(err) {
+ if (err.message === 'some error') {
+ return false
+ }
+ },
+ // jsdom also prints a warning, but we don't care for our use case
+ onConsoleLog() {
+ return false
+ },
+ })
+
+ // Regression #3642
+ expect(vitest.stderr).toBe('')
+})