Skip to content

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:

typescript
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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

bash
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"email": "invalid-email", "name": "John"}'

Incoming Request Log:

json
{
  "level": "info",
  "time": "2025-12-16T10:30:45.100Z",
  "msg": "Incoming request",
  "method": "POST",
  "path": "/api/users"
}

Error Log:

json
{
  "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:

json
{
  "error": {
    "statusCode": 422,
    "name": "UnprocessableEntityError",
    "message": "Invalid email format"
  }
}

Scenario 2: Authentication Error

Request:

bash
curl -X GET http://localhost:3000/api/users/me \
  -H "Authorization: Bearer invalid_token"

Error Log:

json
{
  "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:

json
{
  "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:

typescript
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:

json
{
  "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:

typescript
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:

  1. Controller-level log with business context (user_id, product_id, etc.)
  2. Sequence-level log with request context (HTTP method, path, IP, etc.)

Both perspectives help debug issues faster!

Querying Error Logs

All Errors

logql
{service="b3api"} | json | level="error"

Errors by Endpoint

logql
{service="b3api"} | json | level="error" | path="/api/users"

Errors by Type

logql
{service="b3api"} | json | level="error" | error_name="ValidationError"

Errors by Status Code

logql
{service="b3api"} | json | level="error" | error_code="500"

Slow Requests (Potential Timeouts)

logql
{service="b3api"} | json | duration_ms > 5000

Search Error Messages

logql
{service="b3api"} | json | level="error" | error=~".*database.*"

Error Rate Over Time

logql
sum(rate({service="b3api"} | json | level="error" [5m]))

Top Error Types

logql
topk(10, sum by (error_name) (count_over_time({service="b3api"} | json | level="error" [1h])))

Errors by Endpoint (Most Problematic)

logql
topk(10, sum by (path) (count_over_time({service="b3api"} | json | level="error" [1h])))

Average Request Duration by Endpoint

logql
avg by (path) (avg_over_time({service="b3api"} | json | duration_ms > 0 [1h]))

Testing the Error Handler

Test 1: Invalid Endpoint

bash
curl http://localhost:3000/api/nonexistent

Expected Error Log:

json
{
  "level": "error",
  "msg": "Request failed",
  "method": "GET",
  "path": "/api/nonexistent",
  "error": "Not Found",
  "error_name": "NotFoundError",
  "error_code": 404
}

Test 2: Invalid JSON

bash
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d "{invalid json}"

Expected Error Log:

json
{
  "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:

typescript
@get('/test-error')
async testError() {
  throw new Error('This is a test error');
}
bash
curl http://localhost:3000/api/test-error

Expected Error Log:

json
{
  "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

bash
# 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 responses

Note: Stack traces are always logged regardless of NODE_ENV, but NODE_ENV controls whether they're included in HTTP error responses.

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.

Syneo/Barcoding Documentation