Validation

Handling Validation Errors

Understand built-in ValidationError (422) handling and customize envelopes with middleware try/catch — they bypass defineTaser().onError().

When incoming request parameters, query strings, headers, or JSON bodies fail validation, Taser.js throws a ValidationError. The runtime converts it to a 422 JSON response automatically.

Validation errors bypass `.onError()`

ValidationError (422) and ResponseValidationError (response contracts, 500) are handled by built-in runtime handlers. They never reach defineTaser().onError(), which is reserved for unhandled server crashes. Customize validation envelopes in middleware instead.


Anatomy of a ValidationError

The ValidationError class exposes the raw issues array returned by your schema validator:

class ValidationError extends Error {
  readonly issues: readonly StandardSchemaV1.Issue[];
}

Each issue typically contains:

  • path: Array of property keys or array indices indicating where the error occurred (for example, ["body", "email"] or ["query", "page"]).
  • message: Human-readable error description (for example, "Invalid email address").

Default 422 Envelope

Without customization, failed input validation returns:

{
  "errors": [
    { "message": "Number must be greater than or equal to 1", "path": ["page"] },
    { "message": "Invalid enum value", "path": ["category"] }
  ]
}

Customizing Validation Envelopes in Middleware

Catch ValidationError in a layout middleware try/catch before it reaches the built-in handler:

src/routes/$.ts
import { ValidationError, middleware, t } from "@taserjs/router";
import { unprocessable } from "@taserjs/router/reply";

const formatValidationErrors = middleware(async (_ctx, next) => {
  try {
    return await next();
  } catch (error) {
    if (error instanceof ValidationError) {
      const formattedErrors: Record<string, string[]> = {};

      for (const issue of error.issues) {
        const fieldName = issue.path?.map(String).join(".") || "root";
        if (!formattedErrors[fieldName]) {
          formattedErrors[fieldName] = [];
        }
        formattedErrors[fieldName].push(issue.message);
      }

      return unprocessable({
        status: 422,
        message: "The given data was invalid.",
        errors: formattedErrors,
      });
    }

    throw error;
  }
});

export default t.layout("/*").use(formatValidationErrors);

Example Custom Error Response

Given a request with invalid query parameters:

curl "http://localhost:3000/products?page=-5&category=unknown"

With the middleware above, the client receives:

{
  "status": 422,
  "message": "The given data was invalid.",
  "errors": {
    "query.page": ["Number must be greater than or equal to 1"],
    "query.category": ["Invalid enum value. Expected 'electronics' | 'clothing' | 'books'"]
  }
}

Auto-Generated 422 Response Schemas

When you attach input schemas (query, params, body) to a route, Taser.js automatically documents a 422 error contract for that endpoint. The typed client SDK also understands that the route can return 422 validation issues.