This application implements a comprehensive, centralized error handling system that provides:
- Consistent Error Responses: All errors follow the same structure
- Type Safety: TypeScript error classes with proper typing
- Developer-Friendly: Clear error messages for debugging
- Plugin-Based: Error handling registered as an Elysia plugin
All errors return a consistent JSON structure:
{
"success": false,
"message": "Human-readable error message",
"data": null,
"errors": {}
}| Field | Type | Description |
|---|---|---|
success |
boolean |
Always false for errors |
message |
string |
Human-readable error description |
data |
null |
Always null for error responses |
errors |
object |
Detailed error information (e.g., validation errors) |
| HTTP Status | Description |
|---|---|
| 401 | User not authenticated |
| 403 | User lacks required permissions |
| HTTP Status | Description |
|---|---|
| 404 | Resource not found |
| HTTP Status | Description |
|---|---|
| 422 | Input validation failed |
| HTTP Status | Description |
|---|---|
| 400 | Malformed or invalid request |
| 429 | Rate limit exceeded |
| HTTP Status | Description |
|---|---|
| 500 | Unexpected server error |
Error classes are located in src/libs/errors/.
Malformed or invalid request.
import { BadRequestError } from "@errors";
throw new BadRequestError("Invalid request format");User is not authenticated or has invalid credentials.
import { UnauthorizedError } from "@errors";
throw new UnauthorizedError("Invalid token");User is authenticated but lacks required permissions.
import { ForbiddenError } from "@errors";
throw new ForbiddenError("Insufficient permissions");Requested resource does not exist.
import { NotFoundError } from "@errors";
throw new NotFoundError("User not found");Input is well-formed but fails a business rule. This is the status for a uniqueness conflict —
a duplicate role name, permission name, or email address — carrying a [{ field, message }] array so
the client can render the error inline. It is never 400 and never 409.
import { UnprocessableEntityError } from "@errors";
throw new UnprocessableEntityError("Validation failed");Response:
{
"success": false,
"message": "Validation failed",
"data": null,
"errors": {
"email": ["Invalid email format"],
"age": ["Must be 18 or older"]
}
}Rate limit exceeded.
import { TooManyRequestError } from "@errors";
throw new TooManyRequestError("Too many requests");The error handler is implemented as an Elysia plugin (src/libs/plugins/error-handler.plugin.ts) that catches all errors and formats them consistently.
The plugin uses Elysia's onError lifecycle hook:
export const ErrorHandlerPlugin = new Elysia({ name: "error-handler" }).onError(
({ code, error, set }) => {
// Handle different error types
// Return consistent error response
},
);Error thrown
↓
Is it a custom error class? → Return with appropriate status code
↓
Is it a validation error? → Return 422 with field errors
↓
Is it a NOT_FOUND error? → Return 404
↓
Generic error → Return 500
import { UnauthorizedError, NotFoundError } from "@errors";
// In a service
const getUserById = async (id: string) => {
const user = await db.query.users.findFirst({
where: eq(users.id, id),
});
if (!user) {
throw new NotFoundError(`User with ID ${id} not found`);
}
return user;
};app.get("/users/:id", async ({ params }) => {
const user = await UserService.findById(params.id);
if (!user) {
throw new NotFoundError("User not found");
}
return ResponseToolkit.success(user);
});Elysia validates request bodies automatically using TypeBox schemas:
app.post("/users", ({ body }) => createUser(body), {
body: t.Object({
email: t.String({ format: "email" }),
password: t.String({ minLength: 8 }),
}),
});Invalid requests are automatically caught and returned as 422 errors.
✅ Good:
if (!user) {
throw new NotFoundError("User not found");
}
if (user.role !== "admin") {
throw new ForbiddenError("Admin access required");
}❌ Bad:
if (!user) {
throw new Error("Not found");
}✅ Good:
throw new NotFoundError(`User with ID ${userId} not found`);❌ Bad:
throw new NotFoundError("Not found");✅ Good:
throw new UnauthorizedError("Invalid credentials");❌ Bad:
throw new UnauthorizedError(`Password ${password} is incorrect`);try {
await externalService.call();
} catch (error) {
logger.error({ error }, "External service failed");
throw new BadRequestError("External service unavailable");
}