KosmoJS generates api/errors.ts file with a working default error handler when you create a source folder. It's a regular file - customize it freely.
π¦ Default Error Handler β
import { ValidationError } from "@kosmojs/core/errors";
import { errorHandlerFactory } from "_/api:factory";
export default errorHandlerFactory(
async function defaultErrorHandler(ctx, next) {
try {
await next();
} catch (error: any) {
const [errorMessage, status] =
error instanceof ValidationError
? [`${error.target}: ${error.errorMessage}`, 400]
: [error.message, error.statusCode || 500];
if (ctx.accepts("json")) {
ctx.status = status;
ctx.body = { error: errorMessage };
} else {
ctx.status = status;
ctx.body = errorMessage;
}
}
},
);import { accepts } from "hono/accepts";
import { HTTPException } from "hono/http-exception";
import { ValidationError } from "@kosmojs/core/errors";
import { errorHandlerFactory } from "_/api:factory";
export default errorHandlerFactory(
async function defaultErrorHandler(error, ctx) {
// HTTPException knows how to render itself
if (error instanceof HTTPException) {
return error.getResponse();
}
const [message, status] =
error instanceof ValidationError
? [`${error.target}: ${error.errorMessage}`, 400]
: [error.message, error.statusCode || 500];
const type = accepts(ctx, {
header: "Accept",
supports: ["application/json", "text/plain"],
default: "text/plain",
});
return type === "application/json"
? ctx.json({ error: message }, status)
: ctx.text(message, status);
},
);The Koa handler is wired into global middleware at api/use.ts via errorHandler slot. The Hono handler is wired into app.onError() in the api/app.ts.
π¨ Customization β
Add logging, monitoring, or structured error responses directly in api/errors.ts:
async function defaultErrorHandler(ctx, next) {
try {
await next();
} catch (error: any) {
ctx.app.emit("error", error, ctx);
// ... rest of error handling
}
}async function defaultErrorHandler(error, ctx) {
console.error(`[${ctx.req.method}] ${ctx.req.path}:`, error);
await reportToSentry(error);
if (error instanceof HTTPException) return error.getResponse();
// ... rest of error handling
}For Koa, you can listen to app-level error events in api/app.ts:
export default appFactory(({ createApp }) => {
const app = createApp();
app.on("error", (error) => {
console.error("API Error:", error);
});
app.use(router.routes());
return app;
});π― Route-Level Overrides (Koa) β
Koa supports overriding the error handler per-route or per-subtree via errorHandler slot:
export default defineRoute(({ use, POST }) => [
use(async (ctx, next) => {
try {
await next();
} catch (error: any) {
ctx.status = error.statusCode || 500;
ctx.body = error.message; // plain text for webhooks
}
}, { slot: "errorHandler" }),
POST(async (ctx) => { /* ... */ }),
]);For multiple routes, use a cascading use.ts:
export default [
use(async (ctx, next) => {
try {
await next();
} catch (error: any) {
ctx.status = error.statusCode || 500;
ctx.body = error.message;
console.error(`Webhook error: ${ctx.path}`, error);
}
}, { slot: "errorHandler" }),
];Hono has a single app.onError() for the entire application - branch on ctx.req.path inside the handler if you need route-specific behavior.
π Let Handlers Fail β
Don't wrap handler logic in try-catch. Let errors propagate to the error handler:
// β unnecessary
GET(async (ctx) => {
try {
const user = await fetchUser(ctx.params.id);
ctx.body = user;
} catch (error) {
ctx.status = 500;
ctx.body = { error: "Failed to fetch user" };
}
});
// β
use ctx.assert / ctx.throw
GET(async (ctx) => {
const user = await fetchUser(ctx.params.id);
ctx.assert(user, 404, "User not found");
ctx.body = user;
});// β unnecessary
GET(async (ctx) => {
try {
const user = await fetchUser(ctx.validated.params.id);
return ctx.json(user);
} catch (error) {
return ctx.json({ error: "Failed to fetch user" }, 500);
}
}),
// β
use HTTPException
GET(async (ctx) => {
const user = await fetchUser(ctx.validated.params.id);
if (!user) throw new HTTPException(404, { message: "User not found" });
return ctx.json(user);
}),β‘ Koa vs Hono - Key Differences β
| Koa | Hono | |
|---|---|---|
| Error model | Middleware try-catch, bubbles up | app.onError() catches everything |
await next() throws? | Yes | No |
| Response style | Mutate ctx.body / ctx.status | Return a Response |
| Per-route override | errorHandler slot | Branch inside app.onError() |