Global Error Handling
The B3API implements automatic error handling at the application sequence level, ensuring every API error is caught, logged, and properly formatted. This provides complete visibility into failures without requiring explicit error handling in every controller.
Overview
All API errors are automatically:
- ✅ Caught at the sequence level (no errors escape)
- ✅ Logged with full context in JSON format
- ✅ Tracked with request duration and metadata
- ✅ Sent to Loki/Grafana for monitoring
- ✅ Formatted into proper HTTP error responses
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Client Request │
│ (HTTP Request) │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ MySequence │
│ (src/sequence.ts) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ try { │ │
│ │ 1. Log incoming request │ │
│ │ 2. Execute middleware chain │ │
│ │ 3. Route to controller │ │
│ │ 4. Execute controller method │ │
│ │ 5. Log successful response │ │
│ │ } catch (error) { │ │
│ │ 6. Log error with full context │ │
│ │ 7. Re-throw for LoopBack error formatting │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Pino Logger → Loki/Grafana │
│ (Structured JSON Logs) │
└─────────────────────────────────────────────────────────────┘Implementation
MySequence Class
Located at src/sequence.ts, the custom sequence class extends LoopBack's MiddlewareSequence:
import { MiddlewareSequence, RequestContext } from '@loopback/rest';
import { logger } from './utils/logger';
export class MySequence extends MiddlewareSequence {
async handle(context: RequestContext) {
const { request, response } = context;
const startTime = Date.now();
// Extract request metadata
const requestInfo = {
method: request.method,
path: request.path,
url: request.url,
ip: request.ip,
user_agent: request.headers['user-agent']
};
// Log incoming request
logger.info({
msg: 'Incoming request',
...requestInfo
});
try {
// Execute the request (routing, middleware, controller logic)
const result = await super.handle(context);
// Calculate request duration
const duration = Date.now() - startTime;
// Log successful completion
logger.info({
msg: 'Request completed',
...requestInfo,
status_code: response.statusCode,
duration_ms: duration,
status: 'success'
});
return result;
} catch (error: any) {
// Calculate request duration
const duration = Date.now() - startTime;
// Log error with full context
logger.error({
msg: 'Request failed',
...requestInfo,
error: error.message,
error_name: error.name,
error_code: error.statusCode || error.code,
stack: error.stack,
duration_ms: duration,
status: 'error'
});
// Re-throw to let LoopBack format the error response
throw error;
}
}
}What Gets Logged
1. Incoming Requests
Every incoming request is logged with metadata:
{
"level": "info",
"time": "2025-12-16T10:30:45.123Z",
"pid": 1234,
"hostname": "api-server-01",
"service": "b3api",
"msg": "Incoming request",
"method": "POST",
"path": "/api/users",
"url": "/api/users?page=1",
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}2. Successful Responses
Successful requests include status code and duration:
{
"level": "info",
"time": "2025-12-16T10:30:45.456Z",
"service": "b3api",
"msg": "Request completed",
"method": "POST",
"path": "/api/users",
"url": "/api/users?page=1",
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"status_code": 201,
"duration_ms": 333,
"status": "success"
}3. Failed Requests
Errors include full error details and stack traces:
{
"level": "error",
"time": "2025-12-16T10:30:45.789Z",
"service": "b3api",
"msg": "Request failed",
"method": "POST",
"path": "/api/users",
"url": "/api/users",
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"error": "Email already exists",
"error_name": "UnprocessableEntityError",
"error_code": 422,
"stack": "Error: Email already exists\n at UserController.create (/app/src/controllers/user.controller.ts:45:13)\n at processTicksAndRejections (internal/process/task_queues.js:95:5)",
"duration_ms": 125,
"status": "error"
}Error Types Caught
The global error handler catches ALL errors, including:
Validation Errors
Input validation failures from request bodies:
{
"level": "error",
"msg": "Request failed",
"path": "/api/users",
"error": "Invalid email format",
"error_name": "ValidationError",
"error_code": 422
}Authentication Errors
Invalid or missing authentication credentials:
{
"level": "error",
"msg": "Request failed",
"path": "/api/users/me",
"error": "Invalid token",
"error_name": "UnauthorizedError",
"error_code": 401
}Authorization Errors
Insufficient permissions for requested operation:
{
"level": "error",
"msg": "Request failed",
"path": "/api/admin/users",
"error": "Insufficient permissions",
"error_name": "ForbiddenError",
"error_code": 403
}Not Found Errors
Requested resources that don't exist:
{
"level": "error",
"msg": "Request failed",
"path": "/api/users/999",
"error": "User not found",
"error_name": "NotFoundError",
"error_code": 404
}Database Errors
Connection failures, query errors, constraint violations:
{
"level": "error",
"msg": "Request failed",
"path": "/api/orders",
"error": "Connection timeout after 5000ms",
"error_name": "DatabaseError",
"error_code": 500
}Business Logic Errors
Custom errors thrown by your services:
{
"level": "error",
"msg": "Request failed",
"path": "/api/orders",
"error": "Insufficient inventory for product SKU-123",
"error_name": "Error",
"error_code": 500
}Unhandled Exceptions
Any unexpected runtime errors:
{
"level": "error",
"msg": "Request failed",
"path": "/api/reports",
"error": "Cannot read property 'id' of undefined",
"error_name": "TypeError",
"error_code": 500
}Example Scenarios
Scenario 1: Validation Error
Request:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"email": "invalid-email", "name": "John"}'Incoming Request Log:
{
"level": "info",
"time": "2025-12-16T10:30:45.100Z",
"msg": "Incoming request",
"method": "POST",
"path": "/api/users"
}Error Log:
{
"level": "error",
"time": "2025-12-16T10:30:45.115Z",
"msg": "Request failed",
"method": "POST",
"path": "/api/users",
"error": "Invalid email format",
"error_name": "ValidationError",
"error_code": 422,
"duration_ms": 15
}HTTP Response:
{
"error": {
"statusCode": 422,
"name": "UnprocessableEntityError",
"message": "Invalid email format"
}
}Scenario 2: Authentication Error
Request:
curl -X GET http://localhost:3000/api/users/me \
-H "Authorization: Bearer invalid_token"Error Log:
{
"level": "error",
"time": "2025-12-16T10:31:00.200Z",
"msg": "Request failed",
"method": "GET",
"path": "/api/users/me",
"error": "Invalid token",
"error_name": "UnauthorizedError",
"error_code": 401,
"duration_ms": 8
}Scenario 3: Database Connection Error
Error Log:
{
"level": "error",
"time": "2025-12-16T10:32:15.500Z",
"msg": "Request failed",
"method": "GET",
"path": "/api/orders",
"error": "connect ETIMEDOUT 10.0.0.100:3306",
"error_name": "Error",
"error_code": 500,
"stack": "Error: connect ETIMEDOUT...",
"duration_ms": 5000
}Scenario 4: Custom Business Logic Error
In your service:
export class OrderService {
async createOrder(orderData: any) {
const inventory = await this.checkInventory(orderData.productId);
if (inventory < orderData.quantity) {
throw new Error(`Insufficient inventory: ${inventory} available, ${orderData.quantity} requested`);
}
// ... rest of order creation
}
}Error Log:
{
"level": "error",
"time": "2025-12-16T10:33:00.000Z",
"msg": "Request failed",
"method": "POST",
"path": "/api/orders",
"error": "Insufficient inventory: 5 available, 10 requested",
"error_name": "Error",
"error_code": 500,
"stack": "Error: Insufficient inventory...",
"duration_ms": 42
}Adding Custom Context in Controllers
While the global error handler logs all errors automatically, you can add additional context in your controllers:
import { createLogger } from '../utils/logger';
export class OrderController {
private logger = createLogger('OrderController');
@post('/orders')
async create(@requestBody() orderData: CreateOrderDto) {
// Additional context logging
this.logger.info({
msg: 'Creating order',
user_id: orderData.userId,
product_id: orderData.productId,
quantity: orderData.quantity,
total_amount: orderData.totalAmount
});
try {
const order = await this.orderService.create(orderData);
this.logger.info({
msg: 'Order created successfully',
order_id: order.id,
user_id: orderData.userId
});
return order;
} catch (err: any) {
// This adds business context to the error
this.logger.error({
msg: 'Order creation failed',
user_id: orderData.userId,
product_id: orderData.productId,
error: err.message,
stack: err.stack
});
// Re-throw to let global handler log with request context
throw err;
}
}
}This produces two log entries when an error occurs:
- Controller-level log with business context (user_id, product_id, etc.)
- Sequence-level log with request context (HTTP method, path, IP, etc.)
Both perspectives help debug issues faster!
Querying Error Logs
All Errors
{service="b3api"} | json | level="error"Errors by Endpoint
{service="b3api"} | json | level="error" | path="/api/users"Errors by Type
{service="b3api"} | json | level="error" | error_name="ValidationError"Errors by Status Code
{service="b3api"} | json | level="error" | error_code="500"Slow Requests (Potential Timeouts)
{service="b3api"} | json | duration_ms > 5000Search Error Messages
{service="b3api"} | json | level="error" | error=~".*database.*"Error Rate Over Time
sum(rate({service="b3api"} | json | level="error" [5m]))Top Error Types
topk(10, sum by (error_name) (count_over_time({service="b3api"} | json | level="error" [1h])))Errors by Endpoint (Most Problematic)
topk(10, sum by (path) (count_over_time({service="b3api"} | json | level="error" [1h])))Average Request Duration by Endpoint
avg by (path) (avg_over_time({service="b3api"} | json | duration_ms > 0 [1h]))Testing the Error Handler
Test 1: Invalid Endpoint
curl http://localhost:3000/api/nonexistentExpected Error Log:
{
"level": "error",
"msg": "Request failed",
"method": "GET",
"path": "/api/nonexistent",
"error": "Not Found",
"error_name": "NotFoundError",
"error_code": 404
}Test 2: Invalid JSON
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d "{invalid json}"Expected Error Log:
{
"level": "error",
"msg": "Request failed",
"method": "POST",
"path": "/api/users",
"error": "Unexpected token i in JSON at position 1",
"error_code": 400
}Test 3: Custom Test Endpoint
Create a test endpoint in any controller:
@get('/test-error')
async testError() {
throw new Error('This is a test error');
}curl http://localhost:3000/api/test-errorExpected Error Log:
{
"level": "error",
"msg": "Request failed",
"method": "GET",
"path": "/api/test-error",
"error": "This is a test error",
"stack": "Error: This is a test error\n at ..."
}Benefits
Complete Visibility
Every error is logged, regardless of where it occurs:
- Controller errors
- Service errors
- Repository errors
- Middleware errors
- Unhandled exceptions
Consistent Format
All error logs use the same JSON structure, making them easy to query and analyze.
Request Context
Error logs include:
- HTTP method and path
- Client IP address
- User agent
- Request duration
- Timestamp
Performance Tracking
The duration_ms field helps identify:
- Slow requests
- Timeout patterns
- Performance degradation
Zero Code Changes
Works automatically for all endpoints without requiring explicit error handling in every controller.
Debugging Made Easy
Full stack traces and error context make debugging production issues straightforward.
Configuration
Environment Variables
# Log level (info recommended for production)
LOG_LEVEL=info
# Node environment (affects error response formatting)
NODE_ENV=production # Hides stack traces in HTTP responses
NODE_ENV=development # Shows detailed errors in responsesNote: Stack traces are always logged regardless of NODE_ENV, but NODE_ENV controls whether they're included in HTTP error responses.
Related Documentation
- Logging Overview - System architecture and components
- Logger Usage Guide - How to use the logger in your code
- Console Interception - Automatic console redirection
Summary
The global error handling system provides comprehensive error visibility with zero configuration. Every API error is automatically:
- ✅ Caught and logged with full context
- ✅ Tracked with request duration
- ✅ Formatted in structured JSON
- ✅ Sent to Loki/Grafana for monitoring
- ✅ Converted to proper HTTP error responses
You can focus on building features while the error handling system ensures complete observability of your API's behavior.