[Bug]: Error converting circular structure to JSON #14840
Description
Version
v29.7.0, v30.0.0-alpha.2
Steps to reproduce
- Create a test file with the below test.
npx jest --workerIdleMemoryLimit=1GB path/to/the.test.js
(note:--workerIdleMemoryLimit
is just to force Jest to use workers, which you could also trigger by running many tests,--watch
, etc.)- test suite crashes with only a circular reference error
Test:
it("tmp", () => {
const e = new Error("yikes")
e.e = e
throw e
})
Expected behavior
I expect to see an error in my code! Which of course happens if you remove e.e = e
.
Note that this works fine without workers, and in some cases it works fine even with them (I haven't figured out why.) It seems that in some code paths Jest effectively destructures the error, getting only the fields it will use (i.e. not e
) and then passing those over to the parent process, but in others it serializes the whole error, which fails.
Actual behavior
The error is as follows:
FAIL ./tmp.test.js
● Test suite failed to run
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Error'
--- property 'e' closes the circle
at stringify (<anonymous>)
at messageParent (../../.npm/_npx/fff1c0483a6bf0c3/node_modules/jest-worker/build/index.js:1572:19)
This gives very little information about what actually went wrong. (
Additional context
Of course in a simple repro the answer is simply "don't do that". But in the real cases I started from, I believe the error was likely thrown from third-party code, in an integration test in a large codebase so it's very hard to know. The circular structure in question was a ClientRequest
, which could be used in many places. So having no stacktrace from Jest makes it very hard to track down.
For now I've hacked around it with the following patch:
diff --git a/node_modules/jest-worker/build/workers/messageParent.js b/node_modules/jest-worker/build/workers/messageParent.js
index 62e2cce..0083989 100644
--- a/node_modules/jest-worker/build/workers/messageParent.js
+++ b/node_modules/jest-worker/build/workers/messageParent.js
@@ -26,7 +26,28 @@ function messageParent(message, parentProcess = process) {
message
]);
} else if (typeof parentProcess.send === 'function') {
- parentProcess.send([_types.PARENT_MESSAGE_CUSTOM, message]);
+ try {
+ parentProcess.send([_types.PARENT_MESSAGE_CUSTOM, message]);
+ } catch (e) {
+ if (!e.message.includes("circular structure to JSON")) {
+ throw e
+ }
+ // These errors are super annoying to debug; remove some information from
+ // the error to try to get it through to the parent process.
+ const { failureDetails } = message[1][1]
+ failureDetails.forEach((err, i) => {
+ // Keep the error message+stack, but drop other fields.
+ failureDetails[i] = new Error(e.message + "\nOriginal error was: " + err.message)
+ if (err.stack) {
+ failureDetails[i].stack = err.stack
+ }
+ })
+ try {
+ parentProcess.send([_types.PARENT_MESSAGE_CUSTOM, message]);
+ } catch (e2) {
+ throw e
+ }
+ }
} else {
throw new Error('"messageParent" can only be used inside a worker');
}
diff --git a/node_modules/jest-worker/build/workers/processChild.js b/node_modules/jest-worker/build/workers/processChild.js
index 1a47f23..0100599 100644
--- a/node_modules/jest-worker/build/workers/processChild.js
+++ b/node_modules/jest-worker/build/workers/processChild.js
@@ -79,7 +79,30 @@ function reportSuccess(result) {
if (!process || !process.send) {
throw new Error('Child can only be used on a forked process');
}
- process.send([_types.PARENT_MESSAGE_OK, result]);
+ try {
+ process.send([_types.PARENT_MESSAGE_OK, result]);
+ } catch (e) {
+ if (!e.message.includes("circular structure to JSON")) {
+ throw e
+ }
+ result.testResults.forEach(testResult => {
+ // These errors are super annoying to debug; remove some information from
+ // the error to try to get it through to the parent process.
+ const { failureDetails } = testResult
+ failureDetails.forEach((err, i) => {
+ // Keep the error message+stack, but drop other fields.
+ failureDetails[i] = new Error(e.message + "\nOriginal error was: " + err.message)
+ if (err.stack) {
+ failureDetails[i].stack = err.stack
+ }
+ })
+ })
+ try {
+ process.send([_types.PARENT_MESSAGE_OK, result]);
+ } catch (e2) {
+ throw e
+ }
+ }
}
function reportClientError(error) {
return reportError(error, _types.PARENT_MESSAGE_CLIENT_ERROR);
Probably the more robust approach is something more like what reportError
does, but this was easier to hack in.
See also specific cases that may be the same/similar issues: #11958, #10577
Environment
System:
OS: macOS 14.2.1
CPU: (10) arm64 Apple M1 Max
Binaries:
Node: 18.17.0 - /nix/store/3pmw2p5lhkl6g572n1gc2la32x97815c-nodejs-18.17.0/bin/node
npm: 9.6.7 - ~/.local/bin/npm
npmPackages:
jest: ^29.7.0 => 29.7.0