NUCBA
27 de enero de 2026
producto

Implementá un sistema de errores global en Express + React sin duplicar código

Estructurá una clase de errores en backend que tu frontend entienda y un custom hook que los maneje sin try-catch en cada componente.

Equipo NUCBA

Equipo NUCBA

8 min de lectura

Implementá un sistema de errores global en Express + React sin duplicar código

Cada vez que agregás un try-catch en un componente de React, estás duplicando lógica. Cada vez que creás un nuevo endpoint en Express y manejás errores con res.status(400).json({ error: 'algo' }), estás improvisando un contrato que tu frontend no entiende. El resultado: código inconsistente, mensajes de error confusos para el usuario y bugs silenciosos que explotan en producción.

La solución no es agregar más validaciones ni más bibliotecas. Es estructurar un sistema de errores que funcione en ambos lados de tu stack, con una clase centralizada en el backend y un custom hook en el frontend que elimine el 90% de los try-catch de tus componentes.

Por qué los errores ad-hoc te están costando tiempo

Cuando tu backend devuelve errores con estructuras inconsistentes, el frontend no tiene forma de predecir qué esperar. Un endpoint responde { error: 'mensaje' }, otro { message: 'mensaje' }, y un tercero devuelve un string directo. Tu código en React se llena de condicionales para manejar cada caso particular.

Esto genera tres problemas concretos:

  • Duplicación de lógica: cada componente maneja errores de forma diferente
  • UX inconsistente: algunos errores se muestran como notificaciones, otros como texto rojo, otros desaparecen
  • Debugging imposible: cuando algo falla, no sabés si el problema está en el backend, en el frontend o en ambos

La arquitectura correcta centraliza la estructura del error en el backend y la interpretación en el frontend. Una sola fuente de verdad.

Estructura del backend: clase de errores con contexto

En Express, necesitás una clase que extienda Error y agregue metadata útil para el frontend. Nada de tirar strings genéricos.

// errors/AppError.js
class AppError extends Error {
  constructor(message, statusCode, errorCode, details = {}) {
    super(message);
    this.statusCode = statusCode;
    this.errorCode = errorCode; // código único para el frontend
    this.details = details; // contexto adicional (campo inválido, etc)
    this.isOperational = true; // distingue errores manejables de bugs
    Error.captureStackTrace(this, this.constructor);
  }
}

// Errores predefinidos para casos comunes
class ValidationError extends AppError {
  constructor(message, details) {
    super(message, 400, 'VALIDATION_ERROR', details);
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} no encontrado`, 404, 'NOT_FOUND', { resource });
  }
}

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

module.exports = { AppError, ValidationError, NotFoundError, UnauthorizedError };

El errorCode es clave. En lugar de parsear strings del mensaje, el frontend puede hacer matching directo contra códigos conocidos. El campo details te permite enviar información estructurada: qué campo falló en una validación, qué recurso no se encontró, etc.

Ahora en tus rutas usás estas clases en lugar de res.status() manual:

// routes/users.js
const { NotFoundError, ValidationError } = require('../errors/AppError');

router.get('/users/:id', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      throw new NotFoundError('Usuario');
    }
    res.json(user);
  } catch (error) {
    next(error); // Express lo atrapa en el middleware global
  }
});

router.post('/users', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    
    if (!email || !password) {
      throw new ValidationError('Datos incompletos', {
        fields: { email: !email, password: !password }
      });
    }
    
    const user = await User.create({ email, password });
    res.status(201).json(user);
  } catch (error) {
    next(error);
  }
});

Middleware global de errores en Express

El último paso del backend es un middleware que capture todos los errores y los serialice con estructura consistente:

// middlewares/errorHandler.js
const errorHandler = (err, req, res, next) => {
  // Si es un error operacional esperado
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      success: false,
      errorCode: err.errorCode,
      message: err.message,
      details: err.details,
    });
  }

  // Si es un error inesperado (bug)
  console.error('ERROR NO MANEJADO:', err);
  
  return res.status(500).json({
    success: false,
    errorCode: 'INTERNAL_ERROR',
    message: 'Ocurrió un error inesperado',
    details: {},
  });
};

module.exports = errorHandler;

Lo agregás al final de tu app.js:

const errorHandler = require('./middlewares/errorHandler');

// ... todas tus rutas

app.use(errorHandler); // siempre al final

Ahora cada error que tire tu backend tiene esta forma:

{
  "success": false,
  "errorCode": "VALIDATION_ERROR",
  "message": "Datos incompletos",
  "details": {
    "fields": { "email": true, "password": false }
  }
}

Custom hook en React para manejo centralizado

Del lado del frontend, necesitás un hook que encapsule toda la lógica de manejo de errores. Nada de try-catch en cada componente.

// hooks/useApiCall.js
import { useState } from 'react';
import { useNotification } from './useNotification'; // tu sistema de notificaciones

const ERROR_MESSAGES = {
  VALIDATION_ERROR: 'Revisá los datos ingresados',
  NOT_FOUND: 'El recurso solicitado no existe',
  UNAUTHORIZED: 'No tenés permiso para realizar esta acción',
  INTERNAL_ERROR: 'Ocurrió un error inesperado. Intentá de nuevo.',
};

export const useApiCall = () => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const { showError } = useNotification();

  const execute = async (apiFunction, options = {}) => {
    const { 
      onSuccess, 
      onError, 
      showNotification = true,
      customErrorMessages = {}
    } = options;

    setLoading(true);
    setError(null);

    try {
      const result = await apiFunction();
      
      if (onSuccess) {
        onSuccess(result);
      }
      
      return { success: true, data: result };
    } catch (err) {
      const errorData = err.response?.data || {};
      const errorCode = errorData.errorCode || 'INTERNAL_ERROR';
      const message = customErrorMessages[errorCode] 
        || ERROR_MESSAGES[errorCode] 
        || errorData.message 
        || 'Error desconocido';

      const errorObject = {
        code: errorCode,
        message,
        details: errorData.details || {},
      };

      setError(errorObject);

      if (showNotification) {
        showError(message);
      }

      if (onError) {
        onError(errorObject);
      }

      return { success: false, error: errorObject };
    } finally {
      setLoading(false);
    }
  };

  return { execute, loading, error };
};

Ahora en tus componentes eliminás los try-catch:

// components/UserProfile.jsx
import { useApiCall } from '../hooks/useApiCall';
import { getUser, updateUser } from '../api/users';

const UserProfile = ({ userId }) => {
  const { execute, loading, error } = useApiCall();
  const [user, setUser] = useState(null);

  useEffect(() => {
    execute(() => getUser(userId), {
      onSuccess: (data) => setUser(data),
    });
  }, [userId]);

  const handleUpdate = async (updates) => {
    const result = await execute(
      () => updateUser(userId, updates),
      {
        onSuccess: (data) => {
          setUser(data);
        },
        customErrorMessages: {
          VALIDATION_ERROR: 'El email ya está en uso',
        },
      }
    );

    if (result.success) {
      console.log('Usuario actualizado');
    }
  };

  if (loading) return <Spinner />;
  if (error?.code === 'NOT_FOUND') return <NotFoundView />;

  return (
    <div>
      {/* tu UI */}
    </div>
  );
};

Cero try-catch. El hook maneja todo: loading, errores, notificaciones. Podés customizar mensajes por componente cuando lo necesitás, pero tenés defaults sensatos para todos los casos.

Checklist para implementar tu sistema de errores

Implementá estos pasos en orden:

  1. Backend: creá la clase AppError y sus variantes (ValidationError, NotFoundError, etc.)
  2. Backend: reemplazá todos los res.status().json() manuales por throw new XError()
  3. Backend: agregá el middleware errorHandler al final de tu app
  4. Frontend: definí el mapeo de errorCode a mensajes en un objeto centralizado
  5. Frontend: creá el hook useApiCall con tu lógica de notificaciones
  6. Frontend: migrá componentes de a uno, empezando por los más simples
  7. Testing: probá cada errorCode con casos reales para verificar que el frontend los interpreta bien

Preguntas frecuentes

¿Qué pasa si necesito manejar un error de forma específica en un componente?

El hook acepta un callback onError donde podés ejecutar lógica custom. Por ejemplo, si un error de validación debe marcar campos específicos en un formulario, lo hacés ahí. El sistema centralizado sigue manejando lo genérico (notificaciones, loading), pero tenés el escape hatch.

¿Cómo integro esto con errores de red (timeout, offline)?

En el catch del hook, los errores sin response.data son errores de red. Podés agregar un caso específico antes del manejo de errores de backend:

if (!err.response) {
  // Error de red
  return { success: false, error: { code: 'NETWORK_ERROR', message: 'Revisá tu conexión' } };
}

¿Necesito tipar esto en TypeScript?

Absolutamente. Definí un tipo para la estructura de error y exportalo desde el backend como contrato:

type ApiError = {
  success: false;
  errorCode: string;
  message: string;
  details: Record<string, any>;
};

Esto hace que el frontend tenga autocompletado y type safety total en el manejo de errores.

¿Te gustó este artículo?

Descubre nuestros cursos y carreras para llevar tus habilidades al siguiente nivel.