Error Handling
Proper error handling is crucial for building robust and reliable web applications. Minima.js provides a flexible and powerful error handling mechanism centered around hooks and helpers.
By default, uncaught exceptions result in a generic 500 Internal Server Error response.
Quick Reference
abort- Throw HTTP errors with status codesredirect- Redirect users to different URLserrorhook - Handle errors at different scopesapp.errorHandler- Global error fallbackHttpError.toJSON- Customize error response formatsendhook - Post-response cleanup (for both success and errors)onError- Request-specific error handling
Throwing Errors
The abort Helper
The abort helper throws HTTP-specific errors with status codes and custom payloads.
import { abort } from "@minimajs/server";
app.get("/users/:id", () => {
const user = findUserById(params.get("id"));
if (!user) {
abort({ error: "User not found" }, 404);
}
return user;
});Common shortcuts:
abort.notFound("User not found"); // 404
abort.badRequest("Invalid input"); // 400
abort.unauthorized("Login required"); // 401
abort.forbidden("Access denied"); // 403Check if error is from abort:
if (abort.is(error)) {
console.log(error.statusCode); // Access status code
}The redirect Helper
Redirect users to different URLs with redirect.
import { redirect } from "@minimajs/server";
app.get("/old-path", () => {
redirect("/new-path"); // 302 temporary redirect
});
app.get("/moved", () => {
redirect("/permanent", true); // 301 permanent redirect
});Error Handling Flow
Handling Errors with Hooks
error Hook Behavior
The error hook intercepts errors and can handle them in four ways:
Four possible outcomes:
- Re-throw or abort (Recommended) - Pass to next error hook or handler
- Return data - Treated as successful
200 OKresponse - Return Response - Sent directly (⚠️ bypasses plugins/CORS)
- Return undefined - Pass to next error hook
Global Error Handler
Handle errors across your entire application:
import { hook, abort } from "@minimajs/server";
app.register(
hook("error", (error) => {
console.error("Error occurred:", error);
if (abort.is(error) && error.statusCode === 404) {
abort({ code: "NOT_FOUND", message: "Resource not found" }, 404);
}
abort({ code: "INTERNAL_ERROR", message: "Server error" }, 500);
})
);Module-Level Error Handler
Handle errors for specific modules with scoped error hooks:
async function adminModule(app: App) {
app.register(
hook("error", (error) => {
console.error("Admin error:", error);
const statusCode = abort.is(error) ? error.statusCode : 500;
abort({ adminError: error.message }, statusCode);
})
);
app.get("/dashboard", () => {
throw new Error("Dashboard failed");
});
}
app.register(adminModule, { prefix: "/admin" });Note: Error hooks execute in LIFO order, with child scopes running before parent scopes. See the diagram above for the hierarchy.
Request-Scoped Error Handler (onError)
Handle errors for a single request with the onError helper:
import { onError } from "@minimajs/server";
app.get("/risky", () => {
onError((err) => {
console.error("Request failed:", err);
});
if (Math.random() > 0.5) {
throw new Error("Random failure!");
}
return { success: true };
});Custom Error Handler (app.errorHandler)
The app.errorHandler is the global fallback when no error hooks handle the error.
import { createApp, response } from "@minimajs/server";
const app = createApp();
app.errorHandler = async (error, ctx) => {
ctx.app.log.error(error);
const isDev = process.env.NODE_ENV === "development";
const statusCode = error instanceof Error && "status" in error ? (error as any).status : 500;
const errorBody = JSON.stringify({
success: false,
error: isDev ? (error instanceof Error ? error.message : String(error)) : "Internal Server Error",
timestamp: new Date().toISOString(),
...(isDev && error instanceof Error && { stack: error.stack }),
});
// Use response() to create response with proper status and headers
return response(errorBody, {
status: statusCode,
headers: { "Content-Type": "application/json" },
});
};Important: Use
response()helper to create responses that preserve headers and context state.
Customizing Error Responses
Overriding toJSON Method
The quickest way to customize error responses is by overriding the static toJSON method on error classes. This method controls how errors are serialized when sent to clients.
Global override for all HTTP errors:
import { HttpError } from "@minimajs/server/error";
// Override toJSON for all HttpError instances globally
HttpError.toJSON = (err: HttpError) => {
return {
success: false,
message: err.response,
statusCode: err.status,
timestamp: new Date().toISOString(),
};
};
// Now all HttpErrors use this format
app.get("/users/:id", () => {
const user = findUser(params.get("id"));
if (!user) {
abort("User not found", 404);
}
return user;
});
// Response: { "success": false, "message": "User not found", "statusCode": 404, "timestamp": "2026-01-10T..." }Create custom error class:
import { HttpError } from "@minimajs/server/error";
// Create custom error class with its own toJSON
class ApiError extends HttpError {
static toJSON(err: ApiError) {
return {
success: false,
error: {
code: err.code || "UNKNOWN_ERROR",
message: err.response,
timestamp: new Date().toISOString(),
},
};
}
constructor(
message: string,
statusCode: number,
public code?: string
) {
super(message, statusCode, { code });
}
}
// Use in routes
app.get("/api/users/:id", () => {
const user = findUser(params.get("id"));
if (!user) {
throw new ApiError("User not found", 404, "USER_NOT_FOUND");
}
return user;
});
// Response: { "success": false, "error": { "code": "USER_NOT_FOUND", "message": "User not found", "timestamp": "2026-01-10T..." } }Override ValidationError for custom validation format:
import { ValidationError } from "@minimajs/schema/error";
import { z } from "zod";
// Override ValidationError.toJSON globally
ValidationError.toJSON = (err: ValidationError) => {
return {
success: false,
error: "Validation failed",
validationErrors: err.issues?.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
code: issue.code,
})),
};
};
// Now all ValidationErrors use this format
app.post("/api/signup", async () => {
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const data = await body();
const result = schema.safeParse(data);
if (!result.success) {
throw ValidationError.createFromZodError(result.error);
}
return { success: true };
});
// Response: { "success": false, "error": "Validation failed", "validationErrors": [...] }Per-Instance Customization
You can also override toJSON per instance for one-off customizations:
app.get("/special", () => {
const error = new HttpError("Special error", 400);
error.toJSON = () => ({ custom: "response", timestamp: Date.now() });
throw error;
});Tip: Overriding
toJSONis preferred over custom error handlers because it:
- Keeps error formatting logic with the error class
- Works consistently across all error hooks and handlers
- Allows different error types to have different formats
- Maintains type safety and code organization
- Can be set once globally at application startup
send Hook
Execute cleanup tasks after a response is sent (for both successful and error responses):
app.register(
hook("send", (response, ctx) => {
// Report errors to monitoring service
if (response.status >= 400) {
reportToSentry(ctx.error, {
url: ctx.request.url,
method: ctx.request.method,
status: response.status,
});
}
})
);Best Practices
- Use
abortshortcuts for common HTTP errors (abort.notFound(),abort.badRequest(), etc.) - Override
toJSONfor custom error response formats instead of custom error handlers - Re-throw errors in hooks to allow other handlers to process them
- Log errors before handling them for debugging and monitoring
- Use scoped error hooks for module-specific error handling
- Keep error messages generic in production to avoid leaking sensitive information
- Create custom error classes for different error types (API errors, validation errors, etc.)