Skip to content

jest-circus crashes with TypeError when test throws a plain object and asyncError is undefined #15996

@alex-all3dp

Description

@alex-all3dp

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:

  1. formatNodeAssertErrors sees no .stack on originalError, assigns error = asyncError (undefined), then crashes on error.message = ...
  2. _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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions