Files
scriptc/tests/corpus/writable-error-callbacks.ts
T
Malte UblandClaude Opus 5.5 cf369f70af Fix write callbacks that take the error argument (W3)
`writable.write(chunk, cb)` with `cb` passed as a function VALUE lost the
completion's error: `lowerWriteCb` checked only the callback's syntax and
lowered every other callback through `lowerExprExpecting(cbArg,
funcOf([], VOID))`, which adapts a one-parameter function down to zero
parameters. The native binary therefore printed `undefined` where Node
prints `null`, or the write's own error (`ERR_STREAM_WRITE_AFTER_END` /
`ERR_STREAM_DESTROYED` for late and destroyed writes).

Lower such callbacks through a new `writable.writeEcb` libCall
`(recv, mode, chunk, encoding, cb)`: a zero-parameter callback keeps the
existing synchronous path, and a callback that declares the error (a dyn
value, or `null | Error`) is wrapped by `scr_stream_ecb_wrap` with the
compiler-built adapter that delivers the error the write completion
carries. The stream unit publishes that error only in its
`SCR_STREAM_WRITE_ERRORS` build, selected by the new `streamWriteErrors`
runtime feature (rule: any `writable.writeEcb`). Every other build
compiles `SCR_WCB_CALL` to the same plain call, so their objects are
unchanged.

The destruction path also matched Node: `afterWrite` calls `cb(null)`
even for a stream destroyed after the write was queued (Node 24
`internal/streams/writable`), so the destroyed `SCR_ST_AFTER_WRITE` tick
passes NULL rather than the stream's stored error.

Corpus: `tests/corpus/writable-error-callbacks.ts` covers success, an
erroring write, a write after end, and a write on a destroyed stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:37:57 -07:00

54 lines
1.8 KiB
TypeScript

// Writable.write completion callbacks that take the error argument: null
// after a successful write, the _write failure, ERR_STREAM_WRITE_AFTER_END
// after end(), and ERR_STREAM_DESTROYED after destroy(). Node orders the
// callback and the 'error' event through its writable state machine; this
// program prints both.
import { Writable } from "node:stream";
function show(label: string): (error?: Error | null) => void {
return (error) => {
const code = error ? (error as NodeJS.ErrnoException).code : undefined;
console.log(
label,
error === null ? "null" : error === undefined ? "undefined" : `${code ?? ""} ${error.message}`,
);
};
}
const sink = new Writable({
write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
callback(chunk.toString() === "bad" ? new Error("rejected") : null);
},
});
sink.on("error", (error) => console.log("error event", error.message));
sink.write("good", show("good"));
sink.write("bad", show("bad"));
const ended = new Writable({
write(_chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
callback();
},
});
ended.on("error", (error) =>
console.log("ended error event", (error as NodeJS.ErrnoException).code),
);
ended.write("first", show("first"));
ended.end();
ended.write("late", show("late"));
const destroyed = new Writable({
write(_chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
callback();
},
});
destroyed.destroy();
destroyed.write("after destroy", show("destroyed"));
// A callback declared without parameters keeps its current lowering.
const plain = new Writable({
write(_chunk: Buffer, _encoding: string, callback: () => void): void {
callback();
},
});
plain.write("x", () => console.log("plain done"));