Fix error serialization in 4 catch blocks where JSON.stringify(error) produces "{}"#341
Fix error serialization in 4 catch blocks where JSON.stringify(error) produces "{}"#341jeanschmidt wants to merge 2 commits intoactions:mainfrom
JSON.stringify(error) produces "{}"#341Conversation
- Replace JSON.stringify(error) with error.message/String() in 4 catch blocks
- Fixes empty '{}' output when serializing Error objects with JSON.stringify
- Add tests covering Error objects, non-Error throwables, and empty-braces regression
JSON.stringify(new Error("msg")) produces "{}" because Error properties
are non-enumerable. This made retry-exhaustion and failure messages
unreadable in execCpToPod, execCpFromPod, waitForJobToComplete, and
waitForPodPhases. The fix uses error.message for Error instances and
String() for non-Error throwables.
Signed-off-by: Jean Schmidt <[email protected]>
There was a problem hiding this comment.
Pull request overview
Fixes error serialization in the k8s package so thrown errors from key retry/terminal paths include meaningful messages instead of "{}", improving debuggability in CI logs.
Changes:
- Replace
JSON.stringify(error)witherror instanceof Error ? error.message : String(error)in 4 catch blocks. - Add Jest tests to validate error messages for Error and non-Error throwables across affected functions.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
packages/k8s/src/k8s/index.ts |
Updates 4 thrown error messages to properly serialize Error instances (and non-Error throwables). |
packages/k8s/tests/error-serialization-test.ts |
Adds regression tests covering the updated thrown error messages and retry exhaustion behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| } catch (error) { | ||
| throw new Error(`job ${jobName} has failed: ${JSON.stringify(error)}`) | ||
| throw new Error(`job ${jobName} has failed: ${error instanceof Error ? error.message : String(error)}`) |
There was a problem hiding this comment.
This wrap can produce duplicated messages when the underlying error already includes job ${jobName} has failed (e.g. from isJobSucceeded), resulting in job X has failed: job X has failed. Consider rethrowing the original error in that case, or changing the wrapper message to add new context (and/or use new Error(msg, { cause: error }) to preserve the original error).
| throw new Error(`job ${jobName} has failed: ${error instanceof Error ? error.message : String(error)}`) | |
| const jobFailedMessage = `job ${jobName} has failed` | |
| if (error instanceof Error && error.message.includes(jobFailedMessage)) { | |
| throw error | |
| } | |
| throw new Error( | |
| `${jobFailedMessage}: ${ | |
| error instanceof Error ? error.message : String(error) | |
| }` | |
| ) |
There was a problem hiding this comment.
Technically valid observation: isJobSucceeded throws "job X has failed", and the catch wraps it as "job X has failed: job X has failed". But this is pre-existing behavior — it was "job X has failed: {}" before. Our
PR just makes the duplication visible instead of hiding it behind {}. I'd say this is out of scope for a bug-fix PR — it's a separate improvement. The suggested fix also adds complexity (conditional rethrow) that goes beyond the intent of this change.
| throw new Error( | ||
| `Pod ${podName} is unhealthy with phase status ${phase}: ${JSON.stringify(error)}` | ||
| `Pod ${podName} is unhealthy with phase status ${phase}: ${error instanceof Error ? error.message : String(error)}` | ||
| ) |
There was a problem hiding this comment.
When the pod enters an unhealthy phase, the inner throw already uses the same prefix (Pod ... is unhealthy with phase status ${phase}), so this wrapper will duplicate the message (...: Pod ...). Consider only wrapping errors that come from getPodPhase/backoff (and rethrowing the unhealthy-phase error as-is), or detect the already-prefixed message to avoid duplication. Also consider setting the original error as { cause: error } to preserve context.
There was a problem hiding this comment.
Same reasoning. The inner throw at line 692 uses the same prefix, so the outer catch wraps it redundantly. Again pre-existing, just newly visible. Out of scope.
- Replace rejects.toThrow with rejects.toMatchObject for the empty-braces assertion in execCpToPod test - Remove unused stream-buffers import The toThrow(expect.not.objectContaining(...)) pattern does not reliably negate partial matchers. toMatchObject with expect.not.stringContaining is the idiomatic Jest way to assert a message does NOT contain a substring. Signed-off-by: Jean Schmidt <[email protected]>
Summary
Fix error serialization in 4 catch blocks where
JSON.stringify(error)produces"{}"instead of the actual error message, making thrown errors useless for debugging.Problem
JavaScript's
Errorobject stores itsmessageandstackproperties as non-enumerable properties.JSON.stringify()only serializes enumerable properties, so:This means every error thrown from these 4 locations produces a message like:
The actual error information — the message explaining what failed and why — is silently discarded. When these errors surface in CI logs, operators see that something failed 30 times but have no idea what the underlying error was. This makes debugging pod copy failures, job failures, and unhealthy pod conditions significantly harder than it needs to be.
Affected locations
All 4 are in
packages/k8s/src/k8s/index.ts:execCpToPodexecCpFromPodwaitForJobToCompletewaitForPodPhasesThese are all terminal error paths — the error is thrown up to the GitHub Actions runner, which logs it and fails the job. The error message is the only diagnostic information available to the workflow author.
Solution
Replace
JSON.stringify(error)with:This handles both cases:
Errorobjects (the common case): extracts.messagedirectly, producing readable output like"connection refused","ETIMEOUT","404 Not Found".String()coercion, which calls.toString()and always produces something readable. This is strictly better thanJSON.stringify()even for non-Error objects sinceString({})→"[object Object]"whileJSON.stringify({custom: "data"})would work — but the Error case is what matters in practice.After this fix
What this does NOT change
core.debug()calls earlier in each catch block (e.g. line 429:core.debug(`cpToPod: Attempt ${attempt + 1} failed: ${error}`)) — these already use template literal coercion which calls.toString()and works correctly. Only the finalthrow new Error(...)messages were broken.Files changed
packages/k8s/src/k8s/index.ts— 4 lines changed (identical substitution in each)Test plan
npm run buildsucceeds (tsc + ncc)JSON.stringify(error)in the file