Repository navigation
Description
Summary
A class defined inside a compilePackages dependency that extends the
built-in Error fails instanceof Error, even though its prototype chain
visibly contains Error. The same subclass written in application code works.
The practical effect is that every framework which routes errors with
err instanceof Error silently drops these. In our case a Zod validation
failure inside a Hono handler returns an empty 500 with content-type: text/plain and never reaches app.onError — where Node returns a 400 with a
JSON problem body.
Environment
| Perry | d36a1af0c (2026-09-05), reports 0.5.1520 |
| Platform | Linux x86_64 |
| zod 4.x · hono 4.13.4 · @hono/node-server 1.19.17 |
Repro
import { z } from "zod";
class Mine extends Error {}
try { z.object({ n: z.number() }).parse({ n: "no" }); } catch (e: any) {
console.log("instanceof z.ZodError =", e instanceof z.ZodError);
console.log("instanceof Error =", e instanceof Error);
console.log("proto chain =", /* walk */);
}
console.log("own subclass =", new Mine("x") instanceof Error);
console.log("plain Error =", new Error("y") instanceof Error);| node | perry | |
|---|---|---|
zodError instanceof z.ZodError |
true | true |
zodError instanceof Error |
true | false |
zodError prototype chain |
ZodError → Error → Object |
ZodError → Error → Object |
new Mine("x") instanceof Error (app code) |
true | true |
new Error("y") instanceof Error |
true | true |
The chain contains Error and instanceof Error is still false, so the
Error the compiled package closes over is not the Error the application's
instanceof tests against.
How it surfaces
Hono dispatches to onError only for values it considers errors. Throwing
from a handler, same build:
plain status=500 type=application/json body={"handled":"Error"} ← onError ran
subclass status=500 type=application/json body={"handled":"AppProblem"} ← onError ran
async-plain status=500 type=application/json body={"handled":"Error"} ← onError ran
string status=500 type=text/plain body="" ← onError skipped
zod status=500 type=text/plain body="" ← onError skipped
async-zod status=500 type=text/plain body="" ← onError skipped
A thrown ZodError behaves exactly like a thrown string. Under Node all six
reach onError.
Impact
Every request whose body or query fails validation becomes an opaque 500 with
an empty body and nothing logged — the error never reaches the handler that
would have logged it. From the client's side a typo in a request is
indistinguishable from a server fault, and from the server's side it is
invisible.
That is the whole public API surface of an app that validates with Zod. It cost
us an afternoon on a bid endpoint that was returning 500 for what turned out to
be a malformed clientRef.
Note
z.ZodError itself works — instanceof z.ZodError is true — so an application
that checks the specific class first still behaves. The failure is only for code
that generalises over Error, which is what frameworks do.
Activity
proggeramlug commentedon Sep 7, 2026
Fixed by #9946, landed on
mainvia merge train #9959. The train merged as its own branch, so the close-keyword never fired — closing manually, verified on main.