Error Handling#
Built-in HTTP error classes and custom error handler. Throw errors in route handlers and middlewares - Buntok catches them automatically and returns proper JSON responses. You do NOT need try/catch in every handler.
Built-in Error Classes#
| Class | Status | Usage |
|---|---|---|
| BadRequestError | 400 | Invalid request data |
| UnauthorizedError | 401 | Authentication required |
| ForbiddenError | 403 | Access denied |
| NotFoundError | 404 | Resource not found |
| MethodNotAllowedError | 405 | HTTP method not allowed |
| ConflictError | 409 | Resource conflict (duplicate, etc.) |
| UnprocessableEntityError | 422 | Business logic validation |
| TooManyRequestsError | 429 | Rate limit exceeded |
| InternalServerError | 500 | Server error |
| ServiceUnavailableError | 503 | Service temporarily down |
Usage#
typescript
import {
NotFoundError,
ForbiddenError,
ConflictError,
BadRequestError,
} from "@buntok/core/helpers";
app.get("/users/:id", async (ctx) => {
const user = await db.user.findUnique({ where: { id: ctx.params.id } });
if (!user) throw new NotFoundError("User not found");
return ctx.json(user);
});
app.delete("/users/:id", async (ctx) => {
if (!isAdmin) throw new ForbiddenError("Admin only");
await db.user.delete({ where: { id: ctx.params.id } });
return ctx.json({ deleted: true });
});
app.post("/users", async (ctx) => {
const existing = await db.user.findUnique({ where: { email } });
if (existing) throw new ConflictError("Email already registered");
// ...
});Error Response Format#
typescript
// All HttpError classes return this format:
{
"success": false,
"error": "NotFound", // Error name (camelCase → PascalCase)
"message": "User not found" // Your custom message
}Production message masking
In production (
NODE_ENV=production), the default handler always shows the message of a 4xx HttpError - it was thrown on purpose (e.g. NotFoundError("User not found")). Everything else - 5xx HttpError and unexpected (non-HttpError) errors - is replaced with "An unexpected error occurred" so internal details never leak. Outside production, the original message is always shown.Custom Error Handler#
Override the default error handler to customize error responses:
typescript
const app = new Buntok();
app.onError((error, ctx) => {
console.error(error);
// Custom error format
return ctx.json({
error: {
name: error.name,
message: error.message,
stack: process.env.NODE_ENV === "development" ? error.stack : undefined,
},
}, error instanceof HttpError ? error.status : 500);
});Custom Error Classes#
Extend HttpError for domain-specific errors:
typescript
import { HttpError } from "@buntok/core/helpers";
// Extend HttpError for custom errors
class ValidationError extends HttpError {
constructor(message: string) {
super(422, message);
this.name = "ValidationError";
}
}
class InsufficientBalanceError extends HttpError {
constructor(required: number, available: number) {
super(400, `Insufficient balance: need ${required}, have ${available}`);
this.name = "InsufficientBalanceError";
}
}
// Usage
app.post("/transfer", async (ctx) => {
const { amount } = await ctx.body();
const balance = await getBalance(ctx.user.id);
if (amount > balance) {
throw new InsufficientBalanceError(amount, balance);
}
// ...
});Try/Catch#
You can also catch errors manually for custom handling:
typescript
app.post("/payment", async (ctx) => {
try {
const result = await processPayment(await ctx.body());
return ctx.json({ success: true, ...result });
} catch (error) {
if (error instanceof HttpError) {
return ctx.json({ error: error.message }, error.status);
}
return ctx.json({ error: "Payment failed" }, 500);
}
});Info
You don't need try/catch for most cases. Just throw an error class and Buntok handles the response automatically.
Next Steps#
- Authentication - 401 Unauthorized errors
- RBAC - 403 Forbidden errors
- Validation - 422 Validation errors