Error Handling
Handle errors with middleware and guards
JustScale provides flexible error handling patterns through middleware, guards, and response helpers. Errors can be handled at multiple levels to create robust, maintainable applications.
Response Error Helper
The simplest way to send error responses is using res.error():
src/controllers/players.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { PlayerService } from '../services/player-service';
export const PlayersController = createController('/players', {
inject: { players: PlayerService },
routes: (services) => ({
getOne: Get('/:id').handle(async ({ params, res }) => {
const player = await services.players.findById(params.id);
if (!player) {
res.error('Player not found', 404);
return;
}
res.json({ player });
}),
}),
});This sends:
JSON
{
"error": "Player not found"
}Validation Errors
Validation middleware automatically handles errors:
src/controllers/players.tsTypeScript
import { z } from 'zod';
import { createController } from '@justscale/core';
import { Post, body } from '@justscale/http/builder';
import { PlayerService } from '../services/player-service';
const CreatePlayerSchema = z.object({
name: z.string().min(1, 'Name is required'),
chips: z.number().positive(),
});
export const PlayersController = createController('/players', {
inject: { players: PlayerService },
routes: (services) => ({
create: Post('/')
.apply(body(CreatePlayerSchema))
.handle(async ({ body, res }) => {
// If validation fails, parseBody calls res.error() and throws
// This handler is never reached with invalid data
const player = await services.players.save(body);
res.json({ player });
}),
}),
});Invalid request returns:
JSON
{
"error": "Validation failed: Name is required"
}Middleware Error Handling
Middleware can throw errors to stop the request:
src/controllers/profile.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { requireAuth } from '../middleware/require-auth';
export const ProfileController = createController('/profile', {
routes: () => ({
get: Get('/')
.use(requireAuth)
.handle(({ user, res }) => {
// Only reached if authentication succeeds
res.json({ user });
}),
}),
});Guards for Authorization
Guards provide a cleaner pattern for authorization checks:
src/controllers/profile.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { IsAuthenticated } from '../guards/is-authenticated';
export const ProfileController = createController('/profile', {
routes: () => ({
get: Get('/')
.guard(IsAuthenticated)
.handle(({ user, res }) => {
res.json({ user });
}),
}),
});Guard Composition
Combine multiple guards:
src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Delete } from '@justscale/http';
import { IsAuthenticated } from '../guards/is-authenticated';
import { IsAdmin } from '../guards/is-admin';
import { UserService } from '../services/user-service';
export const UsersController = createController('/users', {
inject: { users: UserService },
routes: (services) => ({
delete: Delete('/:id')
.guard(IsAuthenticated)
.guard(IsAdmin)
.handle(async ({ params, res }) => {
// Only admins can delete users
await services.users.deleteById(params.id);
res.json({ success: true });
}),
}),
});Service-Level Errors
Throw domain-specific errors from services:
src/controllers/games.tsTypeScript
import { createController } from '@justscale/core';
import { Post, populate } from '@justscale/http';
import { GameRepository } from '../repositories/game-repository';
import { GameService } from '../services/game-service';
import { requireAuth } from '../middleware/require-auth';
export const GamesController = createController('/games', {
inject: { games: GameRepository, gameService: GameService },
routes: (services) => ({
join: Post('/:gameId/join')
.use(populate(services.games, 'game', 'gameId'))
.use(requireAuth)
.handle(async ({ game, user, res }) => {
try {
await services.gameService.joinGame(game.id, user.id);
res.json({ success: true });
} catch (error) {
if (error.message === 'Game is full') {
res.error('Cannot join: game is full', 409);
} else if (error.message === 'Game already started') {
res.error('Cannot join: game already started', 409);
} else {
res.error('Failed to join game', 500);
}
}
}),
}),
});Custom Error Classes
Create typed error classes for better error handling:
src/controllers/players.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { PlayerService } from '../services/player-service';
import { errorHandler } from '../utils/error-handler';
export const PlayersController = createController('/players', {
inject: { playerService: PlayerService },
routes: (services) => ({
getOne: Get('/:id').handle(async ({ params, res }) => {
try {
const player = await services.playerService.getPlayer(params.id);
res.json({ player });
} catch (error) {
errorHandler(error, res);
}
}),
}),
});HTTP Status Codes
Use appropriate status codes for different error types:
- 400 - Bad request (client error)
- 401 - Unauthorized (missing/invalid auth)
- 403 - Forbidden (insufficient permissions)
- 404 - Not found
- 409 - Conflict (e.g., duplicate resource)
- 422 - Unprocessable entity (validation)
- 500 - Internal server error
Error Response Formats
Simple Error
example.tsTypeScript
res.error('Not found', 404);JSON
{
"error": "Not found"
}Detailed Error
example.tsTypeScript
res.json({
error: 'Validation failed',
details: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'password', message: 'Too short' },
],
}, 422);JSON
{
"error": "Validation failed",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "password", "message": "Too short" }
]
}Best Practices
- Fail fast - Validate early with middleware and guards
- Use custom error classes - Type-safe error handling
- Be consistent - Use the same error format across your API
- Don't leak sensitive info - Hide stack traces in production
- Log errors - Always log unexpected errors for debugging
- Use appropriate status codes - Help clients understand errors
utils/error-response.tsTypeScript
interface ErrorResponse {
error: string;
details?: unknown;
code?: string;
}
const sendError = (res: any, message: string, status: number, details?: unknown) => {
const response: ErrorResponse = { error: message };
if (details) {
response.details = details;
}
res.json(response, status);
};