Skip to main content

Exception Handling

Muzu provides a comprehensive exception handling system for managing errors in your application.

Basic Exception Handling

Throw exceptions to return error responses:

import { Controller, Get, HttpException } from 'muzu';

@Controller('/users')
class UserController {
@Get(':id')
getUser(req: Request) {
const user = findUserById(req.params.id);

if (!user) {
throw new HttpException('User not found', 404);
}

return user;
}
}

Response:

{
"status": 404,
"message": "User not found",
"kind": "MuzuException"
}

Built-in Exceptions

Muzu provides pre-built exception classes:

HttpException

General HTTP exceptions:

throw new HttpException('Something went wrong', 500);
throw new HttpException('Unauthorized', 401);
throw new HttpException('Forbidden', 403);

NotFoundException

404 errors:

import { NotFoundException } from 'muzu';

@Get(':id')
getUser(req: Request) {
const user = findUserById(req.params.id);

if (!user) {
throw new NotFoundException('User not found');
}

return user;
}

Response:

{
"status": 404,
"message": "User not found",
"kind": "MuzuException"
}

BadRequestException

400 errors:

import { BadRequestException } from 'muzu';

@Post()
createUser(req: Request) {
if (!req.body.email) {
throw new BadRequestException('Email is required');
}

return { created: true };
}

ValidationException

Validation errors (automatically thrown by validators):

import { ValidationException } from 'muzu';

// Automatically thrown by @ValidateBody
@Post()
@ValidateBody(CreateUserDto)
createUser(req: Request) {
// If validation fails, ValidationException is thrown
return { created: true };
}

Response:

{
"status": 400,
"message": "Body validation failed",
"kind": "MuzuException",
"errors": [
{
"field": "email",
"constraint": "isEmail",
"value": "invalid",
"message": "email must be a valid email"
}
]
}

Custom Exceptions

Create custom exception classes:

import { HttpException } from 'muzu';

class UserNotFoundException extends HttpException {
constructor(userId: string) {
super(`User with ID ${userId} not found`, 404, { userId });
}
}

class InsufficientPermissionsException extends HttpException {
constructor(action: string) {
super(`Insufficient permissions to ${action}`, 403, { action });
}
}

class RateLimitException extends HttpException {
constructor() {
super('Rate limit exceeded', 429, {
retryAfter: 60
});
}
}

Using Custom Exceptions

@Controller('/users')
class UserController {
@Get(':id')
getUser(req: Request) {
const user = findUserById(req.params.id);

if (!user) {
throw new UserNotFoundException(req.params.id);
}

return user;
}

@Delete(':id')
deleteUser(req: Request) {
if (!req.user.isAdmin) {
throw new InsufficientPermissionsException('delete users');
}

deleteUser(req.params.id);
return { deleted: true };
}
}

Exception with Details

Add details to exceptions:

throw new HttpException('Validation failed', 400, {
errors: ['Email is invalid', 'Password too short']
});

throw new NotFoundException('User not found', {
userId: req.params.id,
searchedAt: new Date().toISOString()
});

Response:

{
"status": 404,
"message": "User not found",
"kind": "MuzuException",
"details": {
"userId": "123",
"searchedAt": "2024-01-01T12:00:00.000Z"
}
}

HTTP Status Codes

Use the HttpStatus constants:

import { HttpStatus, HttpException } from 'muzu';

// Success codes
res.statusCode = HttpStatus.OK; // 200
res.statusCode = HttpStatus.CREATED; // 201
res.statusCode = HttpStatus.NO_CONTENT; // 204

// Client error codes
throw new HttpException('Bad Request', HttpStatus.BAD_REQUEST); // 400
throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); // 401
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); // 403
throw new HttpException('Not Found', HttpStatus.NOT_FOUND); // 404

// Server error codes
throw new HttpException('Internal Error', HttpStatus.INTERNAL_SERVER_ERROR); // 500

Available Status Codes

HttpStatus.OK                    // 200
HttpStatus.CREATED // 201
HttpStatus.NO_CONTENT // 204
HttpStatus.BAD_REQUEST // 400
HttpStatus.UNAUTHORIZED // 401
HttpStatus.FORBIDDEN // 403
HttpStatus.NOT_FOUND // 404
HttpStatus.METHOD_NOT_ALLOWED // 405
HttpStatus.CONFLICT // 409
HttpStatus.INTERNAL_SERVER_ERROR // 500
HttpStatus.NOT_IMPLEMENTED // 501
HttpStatus.BAD_GATEWAY // 502
HttpStatus.SERVICE_UNAVAILABLE // 503

Exception in Middleware

Throw exceptions in middleware to stop execution:

function AuthMiddleware(req: Request) {
const token = req.headers.authorization;

if (!token) {
throw new HttpException('No token provided', 401);
}

try {
req.user = verifyToken(token);
} catch (error) {
throw new HttpException('Invalid token', 401);
}
}

@Get('/protected')
@Middleware(AuthMiddleware)
getProtected(req: Request) {
// This only runs if AuthMiddleware succeeds
return { user: req.user };
}

Async Exception Handling

Exceptions in async handlers are automatically caught:

@Get(':id')
async getUser(req: Request) {
try {
const user = await database.users.findById(req.params.id);

if (!user) {
throw new NotFoundException('User not found');
}

return user;
} catch (error) {
if (error instanceof NotFoundException) {
throw error; // Re-throw Muzu exceptions
}

// Wrap database errors
throw new HttpException('Database error', 500, {
originalError: error.message
});
}
}

Exception Hierarchy

MuzuException (base)
├─ HttpException (general HTTP errors)
├─ NotFoundException (404)
├─ BadRequestException (400)
└─ ValidationException (400 with errors array)

Exception Properties

class MuzuException {
status: number; // HTTP status code
message: string; // Error message
details?: any; // Optional details object
kind: 'MuzuException'; // Exception type identifier
}

Error Response Format

Standard Exception

{
"status": 404,
"message": "User not found",
"kind": "MuzuException",
"details": {
"userId": "123"
}
}

Validation Exception

{
"status": 400,
"message": "Body validation failed",
"kind": "MuzuException",
"errors": [
{
"field": "email",
"constraint": "isEmail",
"value": "invalid",
"message": "email must be a valid email"
}
]
}
note

ValidationException uses errors array, other exceptions use details object.

Common Exception Patterns

Resource Not Found

@Get(':id')
getResource(req: Request) {
const resource = findById(req.params.id);

if (!resource) {
throw new NotFoundException(`Resource ${req.params.id} not found`);
}

return resource;
}

Authentication Required

@Get('/profile')
getProfile(req: Request) {
if (!req.user) {
throw new HttpException('Authentication required', 401);
}

return { user: req.user };
}

Permission Denied

@Delete(':id')
deleteResource(req: Request) {
if (!req.user.isAdmin) {
throw new HttpException('Admin access required', 403);
}

deleteResource(req.params.id);
return { deleted: true };
}

Invalid Input

@Post()
createResource(req: Request) {
if (!req.body.name) {
throw new BadRequestException('Name is required');
}

if (req.body.name.length < 3) {
throw new BadRequestException('Name must be at least 3 characters');
}

return { created: true };
}

Rate Limiting

@Post()
createResource(req: Request) {
if (isRateLimited(req.user.id)) {
throw new HttpException('Rate limit exceeded', 429, {
retryAfter: 60,
limit: 100
});
}

return { created: true };
}

Exception Best Practices

Use Appropriate Status Codes

// ✅ Good - Correct status codes
throw new HttpException('Unauthorized', 401); // No auth
throw new HttpException('Forbidden', 403); // No permission
throw new NotFoundException('User not found'); // Resource missing
throw new BadRequestException('Invalid input'); // Bad data

// ❌ Bad - Wrong status codes
throw new HttpException('Not found', 500); // Should be 404
throw new HttpException('Unauthorized', 400); // Should be 401

Provide Helpful Messages

// ✅ Good - Descriptive messages
throw new NotFoundException(`User with ID ${id} not found`);
throw new BadRequestException('Email format is invalid');

// ❌ Bad - Vague messages
throw new NotFoundException('Not found');
throw new BadRequestException('Invalid');

Include Relevant Details

// ✅ Good - Helpful details
throw new HttpException('Rate limit exceeded', 429, {
limit: 100,
remaining: 0,
resetAt: Date.now() + 3600000
});

// ❌ Bad - No details
throw new HttpException('Rate limit exceeded', 429);

Don't Expose Sensitive Info

// ✅ Good - Safe error message
throw new HttpException('Authentication failed', 401);

// ❌ Bad - Leaks information
throw new HttpException('Password incorrect for user john@example.com', 401);

Handle All Error Cases

// ✅ Good - All cases handled
@Get(':id')
async getUser(req: Request) {
try {
const user = await database.users.findById(req.params.id);

if (!user) {
throw new NotFoundException('User not found');
}

return user;
} catch (error) {
if (error instanceof NotFoundException) {
throw error;
}

throw new HttpException('Failed to fetch user', 500);
}
}

// ❌ Bad - Unhandled errors
@Get(':id')
async getUser(req: Request) {
const user = await database.users.findById(req.params.id);
return user; // What if database fails?
}

Unknown Errors

Muzu automatically handles unknown errors:

@Get()
getUsers() {
// Unhandled error
throw new Error('Something went wrong');
}

Response:

{
"status": 500,
"message": "Internal Server Error",
"stack": "Error: Something went wrong\n at ..."
}
warning

Stack traces are included in responses. In production, you may want to catch and wrap errors in HttpException to avoid exposing internals.

Next Steps

  • Learn about Middleware for authentication error handling
  • See Validation for automatic validation errors
  • Explore Swagger to document error responses