Error handling en JavaScript: try/catch, tipos de Error y errores personalizados

Manejar errores correctamente es una de las partes más importantes y menos cuidadas del código JavaScript. Un manejo deficiente hace que los errores pasen desapercibidos, que los mensajes no sean descriptivos o que el programa quede en un estado inconsistente. Conocer los tipos de error nativos, cómo crear errores propios y cómo capturarlos en código asíncrono es fundamental.

try/catch/finally

El bloque try contiene el código que puede fallar. catch captura el error. finally siempre se ejecuta, tanto si hubo error como si no:

function parsearJSON(texto) {
  try {
    const datos = JSON.parse(texto);
    return datos;
  } catch (err) {
    // err es el objeto Error lanzado por JSON.parse
    console.error('JSON inválido:', err.message);
    return null;
  } finally {
    // Siempre se ejecuta: ideal para limpieza
    console.log('Intento de parseo completado');
  }
}

console.log(parsearJSON('{"a":1}'));  // { a: 1 }
console.log(parsearJSON('no json')); // null

// Si el bloque finally devuelve un valor, anula el return del try:
function ejemploFinally() {
  try {
    return 'try';
  } finally {
    return 'finally';  // Este es el valor devuelto
  }
}
console.log(ejemploFinally());  // 'finally'

Tipos de Error predefinidos

JavaScript incluye varios tipos de Error especializados. Identificarlos permite actuar de forma diferente según el tipo de fallo:

// TypeError: operación en un tipo incorrecto
try {
  null.propiedad;
} catch (err) {
  console.log(err instanceof TypeError);  // true
  console.log(err.name);                  // 'TypeError'
}

// ReferenceError: variable no declarada
try {
  console.log(variableInexistente);
} catch (err) {
  console.log(err instanceof ReferenceError);  // true
}

// RangeError: valor fuera del rango permitido
try {
  new Array(-1);
} catch (err) {
  console.log(err instanceof RangeError);  // true
}

// SyntaxError: generalmente en eval()
try {
  eval('función rota {');
} catch (err) {
  console.log(err instanceof SyntaxError);  // true
}

// Identificar el tipo en catch:
try {
  // código que puede lanzar varios tipos de error
} catch (err) {
  if (err instanceof TypeError) {
    // error de tipo
  } else if (err instanceof RangeError) {
    // error de rango
  } else {
    throw err;  // Re-lanzar errores desconocidos
  }
}

Crear errores personalizados extendiendo Error

Extender Error permite crear jerarquías de errores propios con información adicional:

class ErrorValidacion extends Error {
  constructor(campo, mensaje) {
    super(mensaje);
    this.name = 'ErrorValidacion';
    this.campo = campo;
  }
}

class ErrorHTTP extends Error {
  constructor(statusCode, mensaje) {
    super(mensaje);
    this.name = 'ErrorHTTP';
    this.statusCode = statusCode;
  }

  esClientError() { return this.statusCode >= 400 && this.statusCode < 500; }
  esServerError() { return this.statusCode >= 500; }
}

// Uso:
function validarEdad(edad) {
  if (typeof edad !== 'number') {
    throw new ErrorValidacion('edad', 'La edad debe ser un número');
  }
  if (edad < 0 || edad > 120) {
    throw new ErrorValidacion('edad', `Edad fuera de rango: ${edad}`);
  }
}

try {
  validarEdad('treinta');
} catch (err) {
  if (err instanceof ErrorValidacion) {
    console.log(`Campo: ${err.campo}, Mensaje: ${err.message}`);
  }
}

Manejo de errores en código asíncrono

// Con async/await: try/catch captura errores de promesas rechazadas
async function obtenerDatos(url) {
  try {
    const r = await fetch(url);
    if (!r.ok) throw new ErrorHTTP(r.status, r.statusText);
    return await r.json();
  } catch (err) {
    if (err instanceof ErrorHTTP && err.esClientError()) {
      console.error('Error del cliente:', err.message);
      return null;
    }
    throw err;  // Re-lanzar errores de servidor o de red
  }
}

// Con Promises: .catch() al final de la cadena
fetch('/api/datos')
  .then(r => r.json())
  .then(procesar)
  .catch(err => {
    console.error('Error en la cadena:', err);
  });

Captura global de errores no manejados

// En el navegador: errores síncronos no capturados
window.onerror = function(mensaje, url, linea, columna, error) {
  console.error('Error global:', { mensaje, url, linea, columna });
  return true;  // true evita que el mensaje aparezca en consola
};

// En el navegador: promesas rechazadas sin .catch()
window.addEventListener('unhandledrejection', (evento) => {
  console.error('Promesa rechazada sin manejar:', evento.reason);
  evento.preventDefault();  // Evita que aparezca en consola
});

// En Node.js:
process.on('uncaughtException', (err) => {
  console.error('Error no capturado:', err);
  process.exit(1);
});

process.on('unhandledRejection', (reason) => {
  console.error('Promesa rechazada sin manejar:', reason);
});

El anti-patrón del catch vacío

Nunca uses un catch vacío o que solo registre el error y siga adelante como si nada. Es uno de los errores más peligrosos porque oculta fallos:

// MAL: oculta el error completamente
try {
  operacionPeligrosa();
} catch (e) {}  // El error desaparece

// MAL: registra pero ignora el estado inconsistente
try {
  operacionPeligrosa();
} catch (e) {
  console.log(e);  // Y sigue adelante como si todo fuera bien
}

// BIEN: actúa según el tipo de error o re-lanza
try {
  operacionPeligrosa();
} catch (e) {
  if (esRecuperable(e)) {
    recuperar();
  } else {
    throw e;  // Re-lanza para que el llamador lo maneje
  }
}

La propiedad err.stack contiene la traza de llamadas en el momento del error y es fundamental para depurar. Asegúrate de registrarla en producción con tu sistema de logging o error tracking.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP