Responses & Errors

Global Error Handling

Catch unhandled runtime crashes with .onError() and customize 404s with .notFound(). Protocol errors (422/415) and response contracts bypass onError.

Taser.js configures error handling on the defineTaser() builder in src/taser.ts. The runnable Hono instance that applies these handlers is compiled inside src/.taserjs/routes.gen.ts.


What .onError() Receives

.onError() is dedicated to unhandled runtime server crashes. It runs only after built-in protocol handlers have already short-circuited:

ErrorDefault statusReaches .onError()?
ValidationError (input schemas)422No — built-in JSON envelope
ResponseValidationError (.returns() contracts)500No — built-in JSON envelope
UnsupportedMediaTypeError415No — built-in JSON envelope
Thrown Responseas thrownNo — returned as-is
Everything else (domain Error, unexpected throws)your handler or 500Yes

To customize 422 validation or response-contract envelopes, catch them in middleware with try/catch (see Handling Validation Errors). Do not expect those errors inside .onError().


Centralized onError Boundary

src/taser.ts
import { defineTaser } from "@taserjs/router";
import { internalServerError, notFound } from "@taserjs/router/reply";
import { context } from "./context.js";

export class ResourceNotFoundError extends Error {
  constructor(public resource: string) {
    super(`${resource} not found`);
    this.name = "ResourceNotFoundError";
  }
}

export default defineTaser()
  .context(context)
  .onError((error, req) => {
    // Domain exceptions thrown from handlers / middleware:
    if (error instanceof ResourceNotFoundError) {
      return notFound({
        status: 404,
        error: "Not Found",
        message: error.message,
      });
    }

    console.error(`[Error on ${req.method} ${req.path}]:`, error);

    const isProduction = process.env.NODE_ENV === "production";
    return internalServerError({
      status: 500,
      error: "Internal Server Error",
      message: isProduction ? "An unexpected error occurred" : (error as Error).message,
    });
  });

Handler signature

.onError((error, req) => Response) receives a TaserRequest as the second argument (path, method, params, query, headers, url, raw), not the full handler ctx.


Customizing 404 Not Found Handling

Use .notFound() when an incoming request does not match any registered route:

src/taser.ts
import { defineTaser } from "@taserjs/router";
import { notFound } from "@taserjs/router/reply";
import { context } from "./context.js";

export default defineTaser()
  .context(context)
  .notFound(({ req }) => {
    return notFound({
      status: 404,
      error: "Not Found",
      message: `The endpoint ${req.method} ${req.path} does not exist on this server.`,
    });
  });

Throwing Errors in Route Handlers

Because .onError() catches unhandled domain and runtime exceptions, you can throw from services or handlers without local try/catch:

src/routes/teams/$id.get.ts
import { json } from "@taserjs/router/reply";
import { t } from "@taserjs/router";
import { ResourceNotFoundError } from "../../errors.js";

export default t.get("/teams/:id").handler(async ({ ctx, req }) => {
  const team = await ctx.db.findTeam(req.params.id);

  if (!team) {
    // Caught by .onError() and mapped to your 404 JSON response:
    throw new ResourceNotFoundError("Team");
  }

  return json(team);
});