Jest version: 30.3.0
What happens
Two code paths in jest-circus crash with TypeError: Cannot set properties of undefined (setting 'message') when a test throws a plain object (not an Error instance) and the captured asyncError is undefined.
Location 1: packages/jest-circus/src/formatNodeAssertErrors.ts
} else {
error = asyncError; // asyncError may be undefined
error.message = ...; // CRASH
}
When originalError has no .stack (e.g. { status: 403, message: 'Forbidden' }), the else branch runs. If asyncError is undefined, writing error.message throws.
Location 2: packages/jest-circus/src/utils.ts (_getError)
// asyncError = errors[1], which may be undefined when errors is an array
if (error && (typeof error.stack === 'string' || error.message)) {
return error;
}
asyncError.message = `thrown: ${prettyFormat(error, {maxDepth: 3})}`; // CRASH
Same crash when errors is a tuple and errors[1] is undefined.
How to reproduce
test('rpc error via done callback', done => {
const { Observable } = require('rxjs');
// subscriber.error() with a plain object (not new Error()) -- common with NestJS RpcException
const obs = new Observable(sub => sub.error({ status: 403, message: 'Forbidden' }));
// no error handler -- unhandled error propagates into jest-circus as [plainObject, undefined]
obs.subscribe(() => done());
});
jest-circus records the error as [{ status: 403, message: 'Forbidden' }, undefined]. During test_done:
formatNodeAssertErrors sees no .stack on originalError, assigns error = asyncError (undefined), then crashes on error.message = ...
_getError hits the same crash on line 436
The test runner aborts with an internal error instead of reporting a clean test failure.
Fix
formatNodeAssertErrors.ts:
} else if (asyncError) {
error = asyncError;
error.message = originalError.message || `thrown: ${prettyFormat(originalError, {maxDepth: 3})}`;
} else {
error = new Error(originalError.message || `thrown: ${prettyFormat(originalError, {maxDepth: 3})}`);
}
utils.ts (_getError):
if (asyncError) {
asyncError.message = `thrown: ${prettyFormat(error, {maxDepth: 3})}`;
return asyncError;
}
return new Error(`thrown: ${prettyFormat(error, {maxDepth: 3})}`);
Both fixes fall back to constructing a new Error when asyncError is undefined.
Notes
Circus.TestError types the tuple second element as Exception (non-optional), but at runtime it can be undefined, so there is also a type/runtime mismatch to fix there.
Jest version: 30.3.0
What happens
Two code paths in jest-circus crash with
TypeError: Cannot set properties of undefined (setting 'message')when a test throws a plain object (not anErrorinstance) and the capturedasyncErrorisundefined.Location 1:
packages/jest-circus/src/formatNodeAssertErrors.tsWhen
originalErrorhas no.stack(e.g.{ status: 403, message: 'Forbidden' }), the else branch runs. IfasyncErrorisundefined, writingerror.messagethrows.Location 2:
packages/jest-circus/src/utils.ts(_getError)Same crash when
errorsis a tuple anderrors[1]isundefined.How to reproduce
jest-circus records the error as
[{ status: 403, message: 'Forbidden' }, undefined]. Duringtest_done:formatNodeAssertErrorssees no.stackonoriginalError, assignserror = asyncError(undefined), then crashes onerror.message = ..._getErrorhits the same crash on line 436The test runner aborts with an internal error instead of reporting a clean test failure.
Fix
formatNodeAssertErrors.ts:utils.ts(_getError):Both fixes fall back to constructing a new
ErrorwhenasyncErrorisundefined.Notes
Circus.TestErrortypes the tuple second element asException(non-optional), but at runtime it can beundefined, so there is also a type/runtime mismatch to fix there.