fix(preload): serialize object-shaped unhandled rejections#2561
Conversation
Plain-object and undefined rejections from the renderer were stringified via String(reason), producing the literal "[object Object]" / "undefined" in main-process logs and discarding all diagnostic content. A serializer now handles each case (Error-like, string/number, object via safe JSON.stringify with circular-ref guard, null/undefined sentinels) so the forwarded message carries the actual payload. Also drops the unused `reason` field from the IPC payload — the main side never read it and unclonable objects could break the send. Refs #2560
There was a problem hiding this comment.
Code Review
This pull request introduces a serializeRejectionReason function in app/browser/preload.js to improve the diagnostic reporting of unhandled promise rejections. This function ensures that various types of rejection reasons, including circular objects, are properly stringified instead of resulting in generic "[object Object]" strings. Feedback suggests wrapping the entire function in a try...catch block to handle potential exceptions when accessing properties like reason.message, ensuring the function remains robust.
| function serializeRejectionReason(reason) { | ||
| if (reason === undefined) return "<undefined>"; | ||
| if (reason === null) return "<null>"; | ||
| if (typeof reason === "string") return reason; | ||
| if (typeof reason !== "object") return String(reason); | ||
| if (typeof reason.message === "string" && reason.message.length > 0) return reason.message; | ||
| try { | ||
| const seen = new WeakSet(); | ||
| return JSON.stringify(reason, (_key, value) => { | ||
| if (typeof value === "object" && value !== null) { | ||
| if (seen.has(value)) return "[Circular]"; | ||
| seen.add(value); | ||
| } | ||
| return value; | ||
| }) ?? "[unserializable rejection]"; | ||
| } catch { | ||
| return "[unserializable rejection]"; | ||
| } | ||
| } |
There was a problem hiding this comment.
This function is very well-written and handles many edge cases. To make it even more robust, consider wrapping the entire function body in a single try...catch block.
Currently, an error could be thrown when accessing reason.message if it's a getter that throws an exception. This would be caught by the outer try...catch in the unhandledrejection event listener, which would prevent the rejection from being reported to the main process.
By wrapping the whole function, any unexpected error during serialization will be caught, and the function will gracefully return '[unserializable rejection]', allowing the rest of the error data (like the stack trace) to still be forwarded.
| function serializeRejectionReason(reason) { | |
| if (reason === undefined) return "<undefined>"; | |
| if (reason === null) return "<null>"; | |
| if (typeof reason === "string") return reason; | |
| if (typeof reason !== "object") return String(reason); | |
| if (typeof reason.message === "string" && reason.message.length > 0) return reason.message; | |
| try { | |
| const seen = new WeakSet(); | |
| return JSON.stringify(reason, (_key, value) => { | |
| if (typeof value === "object" && value !== null) { | |
| if (seen.has(value)) return "[Circular]"; | |
| seen.add(value); | |
| } | |
| return value; | |
| }) ?? "[unserializable rejection]"; | |
| } catch { | |
| return "[unserializable rejection]"; | |
| } | |
| } | |
| function serializeRejectionReason(reason) { | |
| try { | |
| if (reason === undefined) return "<undefined>"; | |
| if (reason === null) return "<null>"; | |
| if (typeof reason === "string") return reason; | |
| if (typeof reason !== "object") return String(reason); | |
| if (typeof reason.message === "string" && reason.message.length > 0) return reason.message; | |
| const seen = new WeakSet(); | |
| return JSON.stringify(reason, (_key, value) => { | |
| if (typeof value === "object" && value !== null) { | |
| if (seen.has(value)) return "[Circular]"; | |
| seen.add(value); | |
| } | |
| return value; | |
| }) ?? "[unserializable rejection]"; | |
| } catch { | |
| return "[unserializable rejection]"; | |
| } | |
| } |
There was a problem hiding this comment.
Applied in 8ce9228. Wrapping the whole body in try/catch is the right defensive call — a throwing reason.message getter would otherwise swallow the rejection entirely.
📦 PR Build Artifacts✅ Build successful! Download artifacts: 🐧 Linuxx86_64 (447.79 MB) - Contains: .deb, .rpm, .tar.gz, .AppImage arm64 (438.04 MB) - Contains: .deb, .rpm, .tar.gz, .AppImage armv7l (415.88 MB) - Contains: .deb, .rpm, .tar.gz, .AppImage 🍎 macOSx86_64 (129.22 MB) - Contains: .dmg 🪟 Windowsx86_64 (109.57 MB) - Contains: .exe installer 📝 Note: Snap packages (.snap) are built in a separate workflow 🕐 Last updated: 2026-05-20 11:17 UTC |
📦 PR Snap Build Artifacts✅ Snap builds successful! Download artifacts: 🐧 Linux Snap Packagesx86_64 (110.66 MB) arm64 (107.48 MB) armv7l (101.54 MB) 📝 Note: Other package formats (.deb, .rpm, .AppImage, .dmg, .exe) are built in the main workflow |
Apply Gemini Code Assist suggestion: wrap the whole function body in try/catch so a throwing `reason.message` getter (or any other unexpected exception during serialization) degrades to "[unserializable rejection]" instead of propagating to the outer handler and dropping the rejection payload entirely. Refs #2560
|



Summary
Plain-object and
undefinedrejections from the renderer were stringified viaString(reason), producing the literal"[object Object]"/"undefined"in main-process logs and discarding all diagnostic content. Teams emits a lot of object-shaped rejections, so a large fraction of[Renderer] Unhandled rejectionlog lines carried no useful information.A small
serializeRejectionReasonhelper now handles each case (Error-like with.message, string/number/boolean, plain object via safeJSON.stringifywith circular-ref guard,null/undefinedsentinels) so the forwarded message carries the actual payload. The unusedreasonfield is also dropped from the IPC payload — the main side never read it and an unclonable object would have broken the wholeipcRenderer.sendcall.First of four fixes from #2560.
Test plan
npm run lint— cleanELECTRON_DEBUG_LOG=true npm startbefore and after. Before: 40 lines containing"[object Object]". After: 0, and rejection messages now show their JSON content (e.g.'{"statusMsg":"the request type for aadGetToken failed due to {...}"}'instead of'[object Object]').Refs #2560