#10857·windmill

bug: bun error wrapper crashes on thrown null and circular objects, losing the original error

Author: S-a-r-i-u-sCreated Aug 26, 2026Updated Aug 26, 2026
Labelswebmux_oneshot

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 responserequestresponse). Any adapter that lets such an error propagate loses it entirely.

To reproduce

Four scripts, all bun, each run once:

1. throw null

typescript
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

typescript
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 structures

3. throw "a string" — survives, but the string is exploded into an index map:

json
{"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:

json
{"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.stringify throws.
  • 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.