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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions