[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(expect): support arbitrary value equality for `toThrow` and make Error detection robust (#9570)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

authored by

Hiroshi Ogawa
Claude Opus 4.5
and committed by
GitHub
(Feb 3, 2026, 5:45 PM +0100) de215c19 728ba617

+118 -15
+1
eslint.config.js
··· 116 116 'ts/method-signature-style': 'off', 117 117 'no-self-compare': 'off', 118 118 'import/no-mutable-exports': 'off', 119 + 'no-throw-literal': 'off', 119 120 }, 120 121 }, 121 122 {
+13 -2
docs/api/expect.md
··· 779 779 780 780 ## toThrowError 781 781 782 - - **Type:** `(received: any) => Awaitable<void>` 782 + - **Type:** `(expected?: any) => Awaitable<void>` 783 783 784 784 - **Alias:** `toThrow` 785 785 ··· 789 789 790 790 - `RegExp`: error message matches the pattern 791 791 - `string`: error message includes the substring 792 - - `Error`, `AsymmetricMatcher`: compare with a received object similar to `toEqual(received)` 792 + - any other value: compare with thrown value using deep equality (similar to `toEqual`) 793 793 794 794 :::tip 795 795 You must wrap the code in a function, otherwise the error will not be caught, and test will fail. ··· 845 845 846 846 test('throws on pineapples', async () => { 847 847 await expect(() => getAsyncFruitStock()).rejects.toThrowError('empty') 848 + }) 849 + ``` 850 + ::: 851 + 852 + :::tip 853 + You can also test non-Error values that are thrown: 854 + 855 + ```ts 856 + test('throws non-Error values', () => { 857 + expect(() => { throw 42 }).toThrowError(42) 858 + expect(() => { throw { message: 'error' } }).toThrowError({ message: 'error' }) 848 859 }) 849 860 ``` 850 861 :::
+12 -3
packages/expect/src/jest-expect.ts
··· 16 16 arrayBufferEquality, 17 17 generateToBeMessage, 18 18 getObjectSubset, 19 + isError, 19 20 iterableEquality, 20 21 equals as jestEquals, 21 22 sparseArrayEquality, ··· 808 809 ) 809 810 } 810 811 811 - if (expected instanceof Error) { 812 + if (isError(expected)) { 812 813 const equal = jestEquals(thrown, expected, [ 813 814 ...customTesters, 814 815 iterableEquality, ··· 837 838 ) 838 839 } 839 840 840 - throw new Error( 841 - `"toThrow" expects string, RegExp, function, Error instance or asymmetric matcher, got "${typeof expected}"`, 841 + const equal = jestEquals(thrown, expected, [ 842 + ...customTesters, 843 + iterableEquality, 844 + ]) 845 + return this.assert( 846 + equal, 847 + 'expected a thrown value to equal #{exp}', 848 + 'expected a thrown value not to equal #{exp}', 849 + expected, 850 + thrown, 842 851 ) 843 852 }, 844 853 )
+18 -3
packages/expect/src/jest-utils.ts
··· 86 86 } 87 87 } 88 88 89 + // https://github.com/jestjs/jest/blob/905bcbced3d40cdf7aadc4cdf6fb731c4bb3dbe3/packages/expect-utils/src/utils.ts#L509 90 + export function isError(value: unknown): value is Error { 91 + if (typeof Error.isError === 'function') { 92 + return Error.isError(value) 93 + } 94 + switch (Object.prototype.toString.call(value)) { 95 + case '[object Error]': 96 + case '[object Exception]': 97 + case '[object DOMException]': 98 + return true 99 + default: 100 + return value instanceof Error 101 + } 102 + }; 103 + 89 104 // Equality function lovingly adapted from isEqual in 90 105 // [Underscore](http://underscorejs.org) 91 106 function eq( ··· 204 219 return false 205 220 } 206 221 207 - if (a instanceof Error && b instanceof Error) { 222 + if (isError(a) && isError(b)) { 208 223 try { 209 224 return isErrorEqual(a, b, aStack, bStack, customTesters, hasKey) 210 225 } ··· 257 272 // - Error names, messages, causes, and errors are always compared, even if these are not enumerable properties. errors is also compared. 258 273 259 274 let result = ( 260 - Object.getPrototypeOf(a) === Object.getPrototypeOf(b) 275 + Object.prototype.toString.call(a) === Object.prototype.toString.call(b) 261 276 && a.name === b.name 262 277 && a.message === b.message 263 278 ) ··· 581 596 function isObjectWithKeys(a: any) { 582 597 return ( 583 598 isObject(a) 584 - && !(a instanceof Error) 599 + && !isError(a) 585 600 && !Array.isArray(a) 586 601 && !(a instanceof Date) 587 602 && !(a instanceof Set)
+4 -3
packages/expect/src/types.ts
··· 8 8 9 9 import type { Test } from '@vitest/runner' 10 10 import type { MockInstance } from '@vitest/spy' 11 - import type { Constructable } from '@vitest/utils' 12 11 import type { Formatter } from 'tinyrainbow' 13 12 import type { AsymmetricMatcher } from './jest-asymmetric-matchers' 14 13 import type { diff, getMatcherUtils, stringify } from './jest-matcher-utils' ··· 535 534 * @example 536 535 * expect(() => functionWithError()).toThrow('Error message'); 537 536 * expect(() => parseJSON('invalid')).toThrow(SyntaxError); 537 + * expect(() => { throw 42 }).toThrow(42); 538 538 */ 539 - toThrow: (expected?: string | Constructable | RegExp | Error) => void 539 + toThrow: (expected?: any) => void 540 540 541 541 /** 542 542 * Used to test that a function throws when it is called. ··· 546 546 * @example 547 547 * expect(() => functionWithError()).toThrowError('Error message'); 548 548 * expect(() => parseJSON('invalid')).toThrowError(SyntaxError); 549 + * expect(() => { throw 42 }).toThrowError(42); 549 550 */ 550 - toThrowError: (expected?: string | Constructable | RegExp | Error) => void 551 + toThrowError: (expected?: any) => void 551 552 552 553 /** 553 554 * Use to test that the mock function successfully returned (i.e., did not throw an error) at least one time
+69 -3
test/core/test/jest-expect.test.ts
··· 484 484 }).toThrow(Error) 485 485 }).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected function to throw an error, but it didn't]`) 486 486 }) 487 + 488 + it('custom error class', () => { 489 + class Error1 extends Error {}; 490 + class Error2 extends Error {}; 491 + 492 + // underlying `toEqual` doesn't require constructor/prototype equality 493 + expect(() => { 494 + throw new Error1('hi') 495 + }).toThrowError(new Error2('hi')) 496 + expect(new Error1('hi')).toEqual(new Error2('hi')) 497 + expect(new Error1('hi')).not.toStrictEqual(new Error2('hi')) 498 + }) 499 + 500 + it('non Error instance', () => { 501 + // primitives 502 + expect(() => { 503 + // eslint-disable-next-line no-throw-literal 504 + throw 42 505 + }).toThrow(42) 506 + expect(() => { 507 + // eslint-disable-next-line no-throw-literal 508 + throw 42 509 + }).not.toThrow(43) 510 + 511 + expect(() => { 512 + expect(() => { 513 + // eslint-disable-next-line no-throw-literal 514 + throw 42 515 + }).toThrow(43) 516 + }).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected a thrown value to equal 43]`) 517 + 518 + // deep equality 519 + expect(() => { 520 + // eslint-disable-next-line no-throw-literal 521 + throw { foo: 'hello world' } 522 + }).toThrow({ foo: expect.stringContaining('hello') }) 523 + expect(() => { 524 + // eslint-disable-next-line no-throw-literal 525 + throw { foo: 'bar' } 526 + }).not.toThrow({ foo: expect.stringContaining('hello') }) 527 + 528 + expect(() => { 529 + expect(() => { 530 + // eslint-disable-next-line no-throw-literal 531 + throw { foo: 'bar' } 532 + }).toThrow({ foo: expect.stringContaining('hello') }) 533 + }).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected a thrown value to equal { foo: StringContaining "hello" }]`) 534 + }) 535 + 536 + it('error from different realm', async () => { 537 + const vm = await import('node:vm') 538 + const context: any = {} 539 + vm.createContext(context) 540 + new vm.Script('fn = () => { throw new TypeError("oops") }; globalObject = this').runInContext(context) 541 + const { fn, globalObject } = context 542 + 543 + // constructor 544 + expect(fn).toThrow(globalObject.TypeError) 545 + expect(fn).not.toThrow(globalObject.ReferenceError) 546 + expect(fn).not.toThrow(globalObject.EvalError) 547 + 548 + // instance 549 + expect(fn).toThrow(new globalObject.TypeError('oops')) 550 + expect(fn).not.toThrow(new globalObject.TypeError('message')) 551 + expect(fn).not.toThrow(new globalObject.ReferenceError('oops')) 552 + expect(fn).not.toThrow(new globalObject.EvalError('no way')) 553 + }) 487 554 }) 488 555 }) 489 556 ··· 1892 1959 // different class 1893 1960 const e1 = new MyError('hello', 'a') 1894 1961 const e2 = new YourError('hello', 'a') 1895 - snapshotError(() => expect(e1).toEqual(e2)) 1896 - expect(e1).not.toEqual(e2) 1897 - expect(e1).not.toStrictEqual(e2) // toStrictEqual checks constructor already 1962 + snapshotError(() => expect(e1).toStrictEqual(e2)) 1963 + expect(e1).toEqual(e2) 1898 1964 assert.deepEqual(e1, e2) 1899 1965 nodeAssert.notDeepStrictEqual(e1, e2) 1900 1966 }
+1 -1
test/core/test/__snapshots__/jest-expect.test.ts.snap
··· 538 538 "custom": "a", 539 539 }", 540 540 "expected": "[Error: hello]", 541 - "message": "expected Error: hello { custom: 'a' } to deeply equal Error: hello { custom: 'a' }", 541 + "message": "expected Error: hello { custom: 'a' } to strictly equal Error: hello { custom: 'a' }", 542 542 } 543 543 `; 544 544