Skip to main content

Error Handling

Rikta provides a robust exception handling system that makes it easy to handle errors consistently across your application.

Introduction​

Proper error handling ensures:

  • Consistent responses - Uniform error format across the API
  • Security - Don't leak sensitive information
  • Debugging - Sufficient information for developers
  • User experience - Clear, actionable error messages

Built-in Exceptions​

Rikta provides built-in HTTP exceptions that map to standard HTTP status codes:

import {
NotFoundException,
BadRequestException,
UnauthorizedException,
ForbiddenException,
InternalServerErrorException,
} from '@riktajs/core';

Using Exceptions​

Throw exceptions in your controllers or services:

import { Controller, Get, Param, NotFoundException } from '@riktajs/core';

@Controller('/users')
export class UserController {
@Autowired()
private userService!: UserService;

@Get('/:id')
async findOne(@Param('id') id: string) {
const user = await this.userService.findById(id);

if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}

return user;
}
}

Exception Response Format​

When an exception is thrown, Rikta returns a JSON response:

{
"statusCode": 404,
"message": "User with ID abc123 not found",
"error": "Not Found"
}

Available Exceptions​

ExceptionStatus CodeUse Case
BadRequestException400Invalid input data
UnauthorizedException401Missing or invalid authentication
ForbiddenException403Authenticated but not authorized
NotFoundException404Resource not found
MethodNotAllowedException405HTTP method not supported
ConflictException409Resource conflict (e.g., duplicate)
UnprocessableEntityException422Semantic validation error
InternalServerErrorException500Server error

Examples​

import {
BadRequestException,
UnauthorizedException,
ForbiddenException,
NotFoundException,
ConflictException,
} from '@riktajs/core';

// Invalid input
if (!isValidEmail(email)) {
throw new BadRequestException('Invalid email format');
}

// Missing authentication
if (!token) {
throw new UnauthorizedException('Authentication required');
}

// Not authorized
if (user.role !== 'admin') {
throw new ForbiddenException('Admin access required');
}

// Resource not found
const post = await this.postService.findById(id);
if (!post) {
throw new NotFoundException('Post not found');
}

// Duplicate resource
const existing = await this.userService.findByEmail(email);
if (existing) {
throw new ConflictException('User with this email already exists');
}

Custom Exceptions​

Create custom exceptions for domain-specific errors:

import { HttpException } from '@riktajs/core';

export class InsufficientFundsException extends HttpException {
constructor(required: number, available: number) {
super(
400,
`Insufficient funds. Required: ${required}, Available: ${available}`
);
this.name = 'InsufficientFundsException';
}
}

export class RateLimitExceededException extends HttpException {
constructor(retryAfter: number) {
super(429, 'Too many requests');
this.name = 'RateLimitExceededException';
}
}

// Usage
throw new InsufficientFundsException(100, 50);
throw new RateLimitExceededException(60);

Exception Filters​

Handle exceptions globally with exception filters:

import { ExceptionFilter, Catch, HttpException } from '@riktajs/core';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, request: FastifyRequest, reply: FastifyReply) {
const status = exception.getStatus();
const message = exception.message;

reply.status(status).send({
statusCode: status,
message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}

Catching All Exceptions​

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, request: FastifyRequest, reply: FastifyReply) {
let status = 500;
let message = 'Internal server error';

if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.message;
} else if (exception instanceof Error) {
message = exception.message;
}

// Log the error
console.error('Unhandled exception:', exception);

reply.status(status).send({
statusCode: status,
message: process.env.NODE_ENV === 'production'
? 'Internal server error'
: message,
timestamp: new Date().toISOString(),
});
}
}

Error Handling in Async Code​

Promise Errors​

Exceptions in async handlers are automatically caught:

@Get('/:id')
async findOne(@Param('id') id: string) {
// If findById throws, it's automatically handled
const user = await this.userService.findById(id);

if (!user) {
throw new NotFoundException(); // Handled
}

return user;
}

Explicit Try-Catch​

For complex error handling:

@Post()
async create(@Body() data: CreateUserDto) {
try {
return await this.userService.create(data);
} catch (error) {
if (error instanceof DuplicateKeyError) {
throw new ConflictException('User already exists');
}
if (error instanceof ValidationError) {
throw new BadRequestException(error.message);
}
// Re-throw for global handler
throw error;
}
}

Validation Errors​

Zod validation errors are automatically formatted:

// When validation fails, returns:
{
"statusCode": 400,
"message": "Validation failed",
"errors": [
{
"path": ["email"],
"message": "Invalid email"
},
{
"path": ["password"],
"message": "String must contain at least 8 character(s)"
}
]
}

Custom Validation Error Response​

import { z } from 'zod';

function formatZodError(error: z.ZodError) {
return {
statusCode: 400,
message: 'Validation failed',
errors: error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
})),
};
}

Service-Level Error Handling​

Handle errors in services with proper exception translation:

@Injectable()
export class UserService {
@Autowired()
private database!: DatabaseService;

async findById(id: string): Promise<User> {
try {
const user = await this.database.query(
'SELECT * FROM users WHERE id = $1',
[id]
);

if (!user) {
throw new NotFoundException(`User ${id} not found`);
}

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

// Wrap database errors
console.error('Database error:', error);
throw new InternalServerErrorException('Failed to fetch user');
}
}

async create(data: CreateUserDto): Promise<User> {
try {
return await this.database.insert('users', data);
} catch (error) {
if (error.code === '23505') { // Unique violation
throw new ConflictException('User already exists');
}
throw new InternalServerErrorException('Failed to create user');
}
}
}

Logging Errors​

Integrate error logging:

@Catch()
export class LoggingExceptionFilter implements ExceptionFilter {
@Autowired()
private logger!: LoggerService;

catch(exception: unknown, request: FastifyRequest, reply: FastifyReply) {
const status = exception instanceof HttpException
? exception.getStatus()
: 500;

// Log based on severity
if (status >= 500) {
this.logger.error('Server error', {
exception,
url: request.url,
method: request.method,
stack: exception instanceof Error ? exception.stack : undefined,
});
} else {
this.logger.warn('Client error', {
status,
message: exception instanceof Error ? exception.message : 'Unknown error',
url: request.url,
});
}

reply.status(status).send({
statusCode: status,
message: status >= 500 ? 'Internal server error' : (exception as Error).message,
timestamp: new Date().toISOString(),
});
}
}

Best Practices​

1. Use Appropriate Status Codes​

// ✅ Good - specific status codes
throw new NotFoundException('User not found'); // 404
throw new ConflictException('Email already taken'); // 409
throw new UnprocessableEntityException('Invalid data'); // 422

// ❌ Avoid - generic errors for specific cases
throw new BadRequestException('Error'); // Too vague

2. Don't Expose Internal Details​

// ✅ Good - safe for production
throw new InternalServerErrorException('Failed to process request');

// ❌ Bad - exposes internals
throw new InternalServerErrorException(
`Database error: connection to postgres:5432 refused`
);

3. Provide Actionable Messages​

// ✅ Good - tells user what to do
throw new BadRequestException(
'Email is required. Please provide a valid email address.'
);

// ❌ Bad - not helpful
throw new BadRequestException('Validation error');

4. Handle All Error Paths​

@Post()
async create(@Body() data: CreateUserDto) {
try {
const user = await this.userService.create(data);
return user;
} catch (error) {
// Handle known errors
if (error instanceof DuplicateError) {
throw new ConflictException('User exists');
}
// Always have a fallback
throw new InternalServerErrorException('Create failed');
}
}

5. Log but Don't Double-Handle​

// ✅ Good - log once at the boundary
@Catch()
export class GlobalFilter implements ExceptionFilter {
catch(exception: unknown, ...) {
this.logger.error(exception);
// respond...
}
}

// ❌ Avoid - logging everywhere
try {
await this.service.doSomething();
} catch (e) {
this.logger.error(e); // Already logged by filter
throw e;
}