Inicio / TypeScript / Node.js Backend con TypeScript / Manejo de errores y logging con Pino

Manejo de errores y logging con Pino

Jerarquía de errores custom, manejador global y logging estructurado con Pino.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Manejo global de errores y logging con Pino

El problema con el manejo de errores ad-hoc

Cuando cada ruta maneja sus propios errores con try/catch y res.status(500).json(...), obtienes:

  • Respuestas de error inconsistentes
  • Sin trazabilidad entre requests
  • Logs mezclados e ilegibles
  • Información sensible filtrada accidentalmente en producción

La solución es un sistema centralizado: jerarquía de errores + error handler global + logger estructurado.


Jerarquía de errores personalizada

// src/errors/AppError.ts
export class AppError extends Error {
  public readonly statusCode: number;
  public readonly isOperational: boolean;

  constructor(
    message: string,
    statusCode = 500,
    isOperational = true
  ) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    // Mantener el nombre correcto en el stack trace
    Object.setPrototypeOf(this, new.target.prototype);
    Error.captureStackTrace(this, this.constructor);
  }
}
// src/errors/NotFoundError.ts
import { AppError } from './AppError';

export class NotFoundError extends AppError {
  constructor(resource = 'Recurso') {
    super(`${resource} no encontrado`, 404);
  }
}
// src/errors/ValidationError.ts
import { AppError } from './AppError';

export class ValidationError extends AppError {
  public readonly fields: Record<string, string[]>;

  constructor(fields: Record<string, string[]>) {
    super('Error de validación', 422);
    this.fields = fields;
  }
}
// src/errors/ConflictError.ts
import { AppError } from './AppError';

export class ConflictError extends AppError {
  constructor(message = 'Conflicto de datos') {
    super(message, 409);
  }
}
// src/errors/UnauthorizedError.ts
import { AppError } from './AppError';

export class UnauthorizedError extends AppError {
  constructor(message = 'No autorizado') {
    super(message, 401);
  }
}

Error handler global de Express

Este middleware captura todos los errores que pasen por next(err):

// src/middlewares/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { ZodError } from 'zod';
import { Prisma } from '@prisma/client';
import { AppError } from '../errors/AppError';
import { ValidationError } from '../errors/ValidationError';
import { logger } from '../lib/logger';

interface ErrorResponse {
  status: 'error';
  message: string;
  statusCode: number;
  fields?: Record<string, string[]>;
  requestId?: string;
}

export function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  _next: NextFunction
): void {
  const requestId = req.headers['x-request-id'] as string;

  // 1. Errores de Zod → ValidationError
  if (err instanceof ZodError) {
    const fields: Record<string, string[]> = {};
    err.errors.forEach(e => {
      const key = e.path.join('.');
      fields[key] = fields[key] ?? [];
      fields[key].push(e.message);
    });

    const response: ErrorResponse = {
      status: 'error',
      message: 'Error de validación',
      statusCode: 422,
      fields,
      requestId,
    };
    logger.warn({ requestId, fields }, 'Validation error');
    res.status(422).json(response);
    return;
  }

  // 2. Errores de Prisma
  if (err instanceof Prisma.PrismaClientKnownRequestError) {
    if (err.code === 'P2002') {
      // Unique constraint violated
      res.status(409).json({
        status: 'error',
        message: 'Ya existe un registro con esos datos',
        statusCode: 409,
        requestId,
      });
      return;
    }
    if (err.code === 'P2025') {
      // Record not found
      res.status(404).json({
        status: 'error',
        message: 'Registro no encontrado',
        statusCode: 404,
        requestId,
      });
      return;
    }
  }

  // 3. Errores operacionales de la app
  if (err instanceof AppError && err.isOperational) {
    const response: ErrorResponse = {
      status: 'error',
      message: err.message,
      statusCode: err.statusCode,
      requestId,
    };
    if (err instanceof ValidationError) {
      response.fields = err.fields;
    }

    logger.warn({ requestId, statusCode: err.statusCode, err: err.message }, 'Operational error');
    res.status(err.statusCode).json(response);
    return;
  }

  // 4. Errores inesperados (bugs)
  logger.error({ requestId, err, stack: err.stack }, 'Unexpected error');

  const statusCode = 500;
  res.status(statusCode).json({
    status: 'error',
    message: process.env.NODE_ENV === 'production'
      ? 'Error interno del servidor'
      : err.message,
    statusCode,
    requestId,
  });
}

Registrarlo como el último middleware en app.ts:

// Al final, después de todas las rutas
app.use(errorHandler);

Logging estructurado con Pino

Pino es el logger más rápido para Node.js. Escribe JSON por defecto, lo que permite integrarlo con sistemas como Datadog, Logtail o CloudWatch.

npm install pino pino-http pino-pretty
npm install -D @types/pino-http
// src/lib/logger.ts
import pino from 'pino';
import { env } from '../config/env';

export const logger = pino({
  level: env.LOG_LEVEL ?? 'info',
  // En desarrollo: formato legible; en producción: JSON puro
  transport:
    env.NODE_ENV === 'development'
      ? { target: 'pino-pretty', options: { colorize: true } }
      : undefined,
  base: {
    service: 'api',
    env: env.NODE_ENV,
  },
  // Redactar campos sensibles
  redact: {
    paths: ['*.password', '*.passwordHash', '*.token', 'req.headers.authorization'],
    censor: '[REDACTED]',
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});

Child loggers con contexto

Un child logger hereda el contexto del padre y añade campos extra. Ideal para inyectar el requestId:

// src/middlewares/requestLogger.ts
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'node:crypto';
import { logger } from '../lib/logger';

export function requestLogger(req: Request, res: Response, next: NextFunction): void {
  const requestId = (req.headers['x-request-id'] as string) ?? randomUUID();
  req.headers['x-request-id'] = requestId;

  // Child logger con requestId en todos los logs de esta request
  const reqLogger = logger.child({ requestId, method: req.method, url: req.url });
  req.log = reqLogger; // Adjuntar al request para usar en controllers

  const start = Date.now();
  res.on('finish', () => {
    reqLogger.info(
      { statusCode: res.statusCode, duration: `${Date.now() - start}ms` },
      'Request completed'
    );
  });

  next();
}

Extender el tipo de Request para el logger:

// src/types/express.d.ts
import { Logger } from 'pino';

declare global {
  namespace Express {
    interface Request {
      log: Logger;
    }
  }
}

Uso en controladores

// En cualquier controlador
export async function getUser(req: Request, res: Response, next: NextFunction) {
  try {
    req.log.info({ userId: req.params.id }, 'Fetching user');
    const user = await userService.findById(req.params.id);
    req.log.info({ userId: user.id }, 'User found');
    res.json(user);
  } catch (err) {
    next(err);
  }
}

Niveles de log y cuándo usarlos

Nivel Uso
trace Depuración muy granular (desactivado en producción)
debug Información de depuración durante desarrollo
info Eventos normales del sistema (peticiones, operaciones exitosas)
warn Situaciones anómalas pero recuperables (errores 4xx, validaciones)
error Errores inesperados que requieren atención (errores 5xx)
fatal El proceso va a terminar

Capturar excepciones no manejadas

// src/server.ts

// Capturar rechazos de promesas no manejados
process.on('unhandledRejection', (reason: unknown) => {
  logger.fatal({ reason }, 'Unhandled Promise Rejection — shutting down');
  process.exit(1);
});

// Capturar excepciones síncronas no atrapadas
process.on('uncaughtException', (err: Error) => {
  logger.fatal({ err }, 'Uncaught Exception — shutting down');
  process.exit(1);
});

Pino con pino-http (alternativa integrada)

import pinoHttp from 'pino-http';
import { logger } from '../lib/logger';

export const httpLogger = pinoHttp({
  logger,
  customLogLevel: (_req, res) => {
    if (res.statusCode >= 500) return 'error';
    if (res.statusCode >= 400) return 'warn';
    return 'info';
  },
  customSuccessMessage: (req, res) =>
    `${req.method} ${req.url} → ${res.statusCode}`,
  redact: ['req.headers.authorization'],
});

// En app.ts:
app.use(httpLogger);

Resumen

  • Jerarquía: AppErrorNotFoundError, ValidationError, ConflictError, UnauthorizedError
  • Error handler global como último middleware en Express
  • Pino para logging estructurado con transport pretty en desarrollo
  • Child loggers con requestId para correlacionar logs de una misma request
  • Redactar campos sensibles con redact de Pino
  • process.on('unhandledRejection') y uncaughtException como red de seguridad
🔒

Ejercicio práctico disponible

Jerarquía de errores y logger estructurado

Desbloquear ejercicios
// Jerarquía de errores y logger estructurado
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes