Summary
When process.emit("error", ...) has no error listeners, Node uses EventEmitter's special unhandled-error semantics: an Error argument is thrown as-is, while missing or non-Error arguments produce an ERR_UNHANDLED_ERROR Error. Perry's process-specific emitter throws the first raw argument, or undefined when no argument is supplied.
Node.js behavior
Observed locally with Node v25.9.0:
for (const [label, fn] of [
["noarg", () => process.emit("error")],
["string", () => process.emit("error", "boom")],
["error", () => process.emit("error", new Error("boom"))],
]) {
try {
console.log(label, "OK", fn());
} catch (err) {
console.log(label, "THROW", err.constructor.name, err.name, err.code, err.message.split("\n")[0]);
}
}
Output:
noarg THROW Error Error ERR_UNHANDLED_ERROR Unhandled error. (undefined)
string THROW Error Error ERR_UNHANDLED_ERROR Unhandled error. ('boom')
error THROW Error Error undefined boom
Current Perry behavior
crates/perry-runtime/src/os.rs::emit_process_event() currently does this when no listeners are registered:
if listeners.is_empty() {
if event == "error" {
crate::exception::js_throw(
args.first()
.copied()
.unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)),
);
}
return false;
}
So process.emit("error") throws JS undefined, and process.emit("error", "boom") throws the string value directly instead of creating an ERR_UNHANDLED_ERROR Error object.
Suggested test surface
process.emit("error") throws an Error with code === "ERR_UNHANDLED_ERROR" and message Unhandled error. (undefined).
process.emit("error", "boom") throws an Error with code === "ERR_UNHANDLED_ERROR" and includes the string in the message.
process.emit("error", err) throws the same Error object when err instanceof Error.
- With an
error listener registered, process.emit("error", value) invokes the listener and returns true without throwing.
Duplicate checks
Searched issues for process emit error unhandled and ERR_UNHANDLED_ERROR process.emit error; searched PRs for process emit error unhandled OR ERR_UNHANDLED_ERROR. No process-specific unhandled error event issue appeared.
Summary
When
process.emit("error", ...)has no error listeners, Node uses EventEmitter's special unhandled-error semantics: an Error argument is thrown as-is, while missing or non-Error arguments produce anERR_UNHANDLED_ERRORError. Perry's process-specific emitter throws the first raw argument, orundefinedwhen no argument is supplied.Node.js behavior
Observed locally with Node v25.9.0:
Output:
Current Perry behavior
crates/perry-runtime/src/os.rs::emit_process_event()currently does this when no listeners are registered:So
process.emit("error")throws JSundefined, andprocess.emit("error", "boom")throws the string value directly instead of creating anERR_UNHANDLED_ERRORError object.Suggested test surface
process.emit("error")throws an Error withcode === "ERR_UNHANDLED_ERROR"and messageUnhandled error. (undefined).process.emit("error", "boom")throws an Error withcode === "ERR_UNHANDLED_ERROR"and includes the string in the message.process.emit("error", err)throws the same Error object whenerr instanceof Error.errorlistener registered,process.emit("error", value)invokes the listener and returnstruewithout throwing.Duplicate checks
Searched issues for
process emit error unhandledandERR_UNHANDLED_ERROR process.emit error; searched PRs forprocess emit error unhandled OR ERR_UNHANDLED_ERROR. No process-specific unhandled error event issue appeared.