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.
All API errors follow the JSend standard:
{
"success" : false ,
"message" : "Descripción del error"
}
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:
Code Name Usage 200 OK Successful GET, PUT, PATCH requests 201 Created Successful POST requests that create resources 204 No Content Successful DELETE requests 400 Bad Request Validation errors, malformed requests 401 Unauthorized Missing or invalid authentication token 403 Forbidden Valid token but insufficient permissions 404 Not Found Resource doesn’t exist 409 Conflict Duplicate resource (e.g., email already exists) 500 Internal Server Error Unexpected server errors
Error Middleware
Location: src/middleware/error.middleware.js
Architecture
The error handling system consists of three main components:
404 Handler - Catches requests to non-existent routes
Global Error Handler - Processes all thrown errors
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: Code Status Message 23505 409 Ya existe un registro con estos datos 23503 400 Referencia a un registro que no existe 23502 400 Falta un campo obligatorio 22P02 400 Formato de datos inválido 42P01 500 Error 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