Skip to main content

Overview

The MaqAgr API implements a centralized error handling system that ensures consistent error responses across all endpoints. All errors follow the JSend specification and are logged with full context for debugging.

Error Response Format

All API errors follow the JSend standard:

Production Format

{
  "success": false,
  "message": "Descripción del error"
}

Development Format

In development mode (NODE_ENV=development), additional debug information is included:
{
  "success": false,
  "message": "Descripción del error",
  "error": {
    "name": "ValidationError",
    "message": "Detalle técnico del error",
    "stack": "Error stack trace completo",
    "code": "23505"
  }
}
Stack traces and internal error details are never exposed in production for security reasons.

HTTP Status Codes

The API uses standard HTTP status codes consistently:
CodeNameUsage
200OKSuccessful GET, PUT, PATCH requests
201CreatedSuccessful POST requests that create resources
204No ContentSuccessful DELETE requests
400Bad RequestValidation errors, malformed requests
401UnauthorizedMissing or invalid authentication token
403ForbiddenValid token but insufficient permissions
404Not FoundResource doesn’t exist
409ConflictDuplicate resource (e.g., email already exists)
500Internal Server ErrorUnexpected server errors

Error Middleware

Location: src/middleware/error.middleware.js

Architecture

The error handling system consists of three main components:
  1. 404 Handler - Catches requests to non-existent routes
  2. Global Error Handler - Processes all thrown errors
  3. AsyncHandler Wrapper - Eliminates try-catch boilerplate

404 Not Found Handler

Automatically handles requests to non-existent routes:
import { notFound } from './middleware/error.middleware.js';

// In app.js - after all routes
app.use(notFound);
Response Example:
{
  "success": false,
  "message": "La ruta /api/invalid/route no existe en este servidor"
}

Global Error Handler

Centralized error processing with intelligent detection:
import { errorHandler } from './middleware/error.middleware.js';

// In app.js - last middleware
app.use(errorHandler);
Features:
  • Automatic JWT error detection (expired, invalid tokens)
  • PostgreSQL error code mapping
  • Contextual logging (4xx = warn, 5xx = error)
  • Request correlation via request-id
  • User context tracking (if authenticated)

Error Types Handled

JsonWebTokenError → 401 Unauthorized
{
  "success": false,
  "message": "Token inválido"
}
TokenExpiredError → 401 Unauthorized
{
  "success": false,
  "message": "Token expirado"
}
Common PostgreSQL error codes are automatically mapped:
CodeStatusMessage
23505409Ya existe un registro con estos datos
23503400Referencia a un registro que no existe
23502400Falta un campo obligatorio
22P02400Formato de datos inválido
42P01500Error de configuración de base de datos
Example:
{
  "success": false,
  "message": "Ya existe un registro con estos datos"
}
ValidationError → 400 Bad RequestSingle error:
{
  "success": false,
  "message": "Error de validación"
}
Multiple errors:
{
  "success": false,
  "message": "Error de validación",
  "errors": [
    "El email es inválido",
    "La contraseña debe tener al menos 8 caracteres"
  ]
}
CastError → 400 Bad RequestOccurs when invalid ID format is provided:
{
  "success": false,
  "message": "Formato de ID inválido"
}

Custom Error Classes

Location: src/utils/errors.util.js (re-exported from middleware)

AppError

Generic application error with custom status code:
import { AppError } from './middleware/error.middleware.js';

throw new AppError('Operación no permitida', 403);
throw new AppError('Recurso no disponible', 503);

ValidationError

For validation failures with detailed error list:
import { ValidationError } from './middleware/error.middleware.js';

const errors = [
  'El email es inválido',
  'La contraseña debe contener una mayúscula'
];

throw new ValidationError(errors);

AuthenticationError

For authentication failures (401):
import { AuthenticationError } from './middleware/error.middleware.js';

if (!user) {
  throw new AuthenticationError('Credenciales inválidas');
}

if (tokenExpired) {
  throw new AuthenticationError('Token expirado');
}

AuthorizationError

For permission failures (403):
import { AuthorizationError } from './middleware/error.middleware.js';

if (user.role_id !== ADMIN_ROLE) {
  throw new AuthorizationError('Se requiere rol de administrador');
}

NotFoundError

For missing resources (404):
import { NotFoundError } from './middleware/error.middleware.js';

const tractor = await getTractorById(id);
if (!tractor) {
  throw new NotFoundError('Tractor no encontrado');
}

AsyncHandler Wrapper

Eliminates the need for try-catch blocks in route handlers:

Without AsyncHandler

export const getTractor = async (req, res, next) => {
  try {
    const tractor = await Tractor.findById(req.params.id);
    if (!tractor) {
      return res.status(404).json({ success: false, message: 'Not found' });
    }
    return res.status(200).json({ success: true, data: tractor });
  } catch (error) {
    next(error);
  }
};

With AsyncHandler

import { asyncHandler } from './middleware/error.middleware.js';
import { successResponse } from './utils/response.util.js';
import { NotFoundError } from './middleware/error.middleware.js';

export const getTractor = asyncHandler(async (req, res) => {
  const tractor = await Tractor.findById(req.params.id);
  
  if (!tractor) {
    throw new NotFoundError('Tractor no encontrado');
  }
  
  return successResponse(res, tractor, 'Tractor obtenido exitosamente');
});
Any error thrown inside asyncHandler is automatically caught and passed to the error middleware.

Error Logging

The error middleware includes intelligent logging based on severity:

4xx Errors (Client Errors)

Logged at WARN level:
logger.warn('[400] Error de validación', {
  requestId: 'abc-123',
  method: 'POST',
  url: '/api/tractors',
  userId: 42,
  userEmail: '[email protected]',
  ip: '192.168.1.1',
  errorName: 'ValidationError'
});

5xx Errors (Server Errors)

Logged at ERROR level with full stack trace:
logger.error('[500] Error interno del servidor', {
  requestId: 'abc-123',
  method: 'GET',
  url: '/api/calculations/power',
  userId: 42,
  ip: '192.168.1.1',
  errorName: 'Error',
  stack: 'Error: Database connection lost\n    at ....',
  pgQuery: 'SELECT * FROM tractors WHERE id = $1',
  pgDetail: 'Connection terminated unexpectedly'
});

Usage Examples

Example 1: Controller with Validation

import { asyncHandler } from '../middleware/error.middleware.js';
import { validationErrorResponse, createdResponse } from '../utils/response.util.js';
import { isValidEmail, isValidPassword } from '../utils/validators.util.js';

export const register = asyncHandler(async (req, res) => {
  const { name, email, password } = req.body;
  
  // Validate input
  const errors = [];
  if (!isValidEmail(email)) errors.push('Email inválido');
  if (!isValidPassword(password)) errors.push('Contraseña débil');
  
  if (errors.length > 0) {
    return validationErrorResponse(res, errors);
  }
  
  // Create user (any errors automatically handled)
  const user = await User.create({ name, email, password });
  const token = generateToken(user);
  
  return createdResponse(res, { user, token }, 'Usuario registrado');
});

Example 2: Custom Error Throwing

import { asyncHandler, NotFoundError, AuthorizationError } from '../middleware/error.middleware.js';
import { successResponse } from '../utils/response.util.js';

export const deleteTractor = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const userId = req.user.user_id;
  
  // Check if tractor exists
  const tractor = await Tractor.findById(id);
  if (!tractor) {
    throw new NotFoundError('Tractor no encontrado');
  }
  
  // Check ownership
  if (tractor.owner_id !== userId && req.user.role_id !== ADMIN_ROLE) {
    throw new AuthorizationError('No tienes permiso para eliminar este tractor');
  }
  
  // Delete tractor
  await Tractor.delete(id);
  
  return successResponse(res, null, 'Tractor eliminado exitosamente');
});

Example 3: Database Error Handling

import { asyncHandler, AppError } from '../middleware/error.middleware.js';
import { conflictResponse, successResponse } from '../utils/response.util.js';
import { pool } from '../config/db.js';

export const createTerrain = asyncHandler(async (req, res) => {
  const { name, soil_type, slope } = req.body;
  
  try {
    const result = await pool.query(
      'INSERT INTO terrains (name, soil_type, slope, user_id) VALUES ($1, $2, $3, $4) RETURNING *',
      [name, soil_type, slope, req.user.user_id]
    );
    
    return successResponse(res, result.rows[0], 'Terreno creado');
  } catch (error) {
    // PostgreSQL unique constraint violation
    if (error.code === '23505') {
      return conflictResponse(res, 'Ya existe un terreno con ese nombre');
    }
    
    // Re-throw for global error handler
    throw error;
  }
});

Best Practices

Prefer custom error classes over generic Error:
// Good
throw new NotFoundError('Recurso no encontrado');

// Avoid
const error = new Error('Not found');
error.statusCode = 404;
throw error;
Always use asyncHandler for async route handlers:
// Good
export const myRoute = asyncHandler(async (req, res) => { ... });

// Avoid
export const myRoute = async (req, res, next) => {
  try { ... } catch (error) { next(error); }
};
Validate input before processing:
export const createResource = asyncHandler(async (req, res) => {
  // Validate first
  if (!isValidEmail(email)) {
    return validationErrorResponse(res, 'Email inválido');
  }
  
  // Then process
  const result = await Resource.create(req.body);
  return createdResponse(res, result);
});
Always use standardized response functions:
// Good
return successResponse(res, data, message);
return notFoundResponse(res, 'Not found');

// Avoid
return res.status(200).json({ success: true, data });
return res.status(404).json({ success: false, message: 'Not found' });
Include relevant metadata in error logs:
logger.error('Payment processing failed', {
  userId: user.id,
  amount: payment.amount,
  paymentId: payment.id,
  error: err.message
});

Testing Error Handling

Example test for error scenarios:
import { describe, it, expect } from '@jest/globals';
import { request } from './helpers/testHelpers.js';

describe('Error Handling', () => {
  it('should return 404 for non-existent resource', async () => {
    const res = await request
      .get('/api/tractors/999999')
      .set('Authorization', `Bearer ${validToken}`);
    
    expect(res.status).toBe(404);
    expect(res.body.success).toBe(false);
    expect(res.body.message).toContain('no encontrado');
  });
  
  it('should return 401 for invalid token', async () => {
    const res = await request
      .get('/api/auth/profile')
      .set('Authorization', 'Bearer invalid_token');
    
    expect(res.status).toBe(401);
    expect(res.body.success).toBe(false);
    expect(res.body.message).toBe('Token inválido');
  });
  
  it('should return 400 for validation errors', async () => {
    const res = await request
      .post('/api/auth/register')
      .send({ email: 'invalid', password: '123' });
    
    expect(res.status).toBe(400);
    expect(res.body.success).toBe(false);
    expect(res.body).toHaveProperty('errors');
  });
});

Utilities

Response utilities and validators

Testing

Testing error scenarios

Build docs developers (and LLMs) love