bug: bun error wrapper crashes on thrown null and circular objects, losing the original error
Describe the bug
The Bun wrapper that turns a thrown value into a job error is not defensive about what was thrown. For two common cases it crashes itself, and the original error is replaced by an internal stack trace from Windmill's own wrapper code.
The circular-object case is the practically important one: several widely used HTTP clients throw errors with circular references (axios attaches response → request → response). Any adapter that lets such an error propagate loses it entirely.
To reproduce
Four scripts, all bun, each run once:
1. throw null
export async function main() { throw null; }Result — the wrapper dereferences e.message on null:
ExecutionErr: exit code for "bun run": 1, last log lines:
43 | } catch(e) {
44 | console.error(e);
45 | let err = { message: e.message, name: e.name, stack: e.stack };
^
TypeError: null is not an object (evaluating 'e.message')2. Circular object
export async function main() {
const a: any = { name: "circular" }; a.self = a;
throw a;
}Result — the wrapper fails serialising it:
ExecutionErr: exit code for "bun run": 1, last log lines:
58 | err["extra"] = extra;
59 | }
60 | await fs.writeFile("result.json", JSON.stringify(err));
^
TypeError: JSON.stringify cannot serialize cyclic structures3. throw "a string" — survives, but the string is exploded into an index map:
{"error":{"extra":{"0":"n","1":"u","2":"r","3":" ","4":"e","5":"i","6":"n",
"7":" ","8":"s","9":"t","10":"r","11":"i","12":"n","13":"g","length":14}}}4. throw {code: 42, hint: "..."} — handled correctly:
{"error":{"extra":{"code":42,"hint":"kein Error"}}}In cases 1 and 2 the job does fail, so this is not silent — but the reported error describes Windmill's wrapper rather than what the script actually threw, which makes the real cause unrecoverable from the run history.
Expected behavior
- Guard the property reads (
e?.message) so a non-object throw does not crash the wrapper. - Serialise with a cycle-safe replacer, or fall back to a truncated description when
JSON.stringifythrows. - For a thrown string, preserve it as a string rather than spreading it into
{"0":"n","1":"u",…}.
In all cases the goal is the same: whatever the script threw should still be identifiable in the result.
Screenshots
No response
Browser information
Not applicable — reproduced via API.
Application version
CE v1.796.0-12-gffdf17ef8d, image ghcr.io/windmill-labs/windmill:main, digest sha256:f8513becee64ac47b0099a84adde4bb90962f396768070671b0ab9790e343f87.
Additional Context
Not tested in the other runtimes — the wrapper shown in the traces is the Bun one, but if the same pattern (e.message / bare JSON.stringify) exists in the Deno or Python wrappers, they are likely affected too.
Context for why this matters beyond tidiness: flows that branch on the error class in an error handler cannot distinguish a rejected approval from a genuine failure if the error has been replaced by a serialisation crash inside the wrapper.
Source: windmill-labs/windmill