Skip to content

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 ​

ts
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;
      }
    }
  },
);
ts
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:

ts
async function defaultErrorHandler(ctx, next) {
  try {
    await next();
  } catch (error: any) {
    ctx.app.emit("error", error, ctx);
    // ... rest of error handling
  }
}
ts
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:

api/app.ts
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:

api/webhooks/github/index.ts
ts
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:

api/webhooks/use.ts
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:

ts
// ❌ 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;
});
ts
// ❌ 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 ​

KoaHono
Error modelMiddleware try-catch, bubbles upapp.onError() catches everything
await next() throws?YesNo
Response styleMutate ctx.body / ctx.statusReturn a Response
Per-route overrideerrorHandler slotBranch inside app.onError()

Released under the MIT License.