Skip to content

node:process: match unhandled error event throwing semantics #3052

@andrewtdiz

Description

@andrewtdiz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions