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 scopesHttpError.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 transform hooks)
- Return undefined - Pass to next error hook
Global Error Handler
Handle errors across your entire application by registering an error hook at the root level:
import { hook, abort } from "@minimajs/server";
app.register(
hook("error", (error) => {
console.error("Error occurred:", error);
// Re-throw HTTP errors with custom format
if (abort.is(error)) {
abort({ code: "HTTP_ERROR", message: error.message }, error.statusCode);
}
// Handle all other errors as 500
abort({ code: "INTERNAL_ERROR", message: "Server error" }, 500);
})
);Note: If no error hook handles the error (all return
undefinedor none are registered), unhandledHttpErrorinstances render themselves, while other errors result in a generic500 Internal Server Errorresponse.
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 };
});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.)