Manejo de Errores

Gestiona los fallos de forma robusta con try/catch/finally, tipos de error nativo y errores personalizados.

Los errores son inevitables: una petición falla, el usuario introduce datos incorrectos, un archivo no existe. El manejo correcto de errores hace la diferencia entre un programa que se rompe silenciosamente y uno que falla de forma controlada y recuperable.


try / catch / finally

try {
  // Código que puede lanzar un error
  const datos = JSON.parse('esto no es json');
} catch (error) {
  // Se ejecuta solo si hay un error en el bloque try
  console.error('Falló el parseo:', error.message);
} finally {
  // Se ejecuta SIEMPRE — con o sin error
  console.log('limpieza o cierre de recursos');
}

finally es útil para liberar recursos, ocultar spinners o cerrar conexiones independientemente del resultado:

async function obtenerDatos(url) {
  mostrarSpinner();

  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (error) {
    console.error(error);
    return null;
  } finally {
    ocultarSpinner();  // se ejecuta pase lo que pase
  }
}

El objeto Error

Cuando se lanza un error, el objeto error tiene propiedades estándar:

try {
  null.propiedad;  // TypeError
} catch (error) {
  console.log(error.name);     // 'TypeError'
  console.log(error.message);  // "Cannot read properties of null..."
  console.log(error.stack);    // stack trace completo — muy útil para depurar
}

También puedes lanzar errores manualmente con throw:

function dividir(a, b) {
  if (b === 0) {
    throw new Error('No se puede dividir por cero');
  }
  return a / b;
}

try {
  dividir(10, 0);
} catch (error) {
  console.error(error.message);  // 'No se puede dividir por cero'
}

throw puede lanzar cualquier valor, pero siempre lanza instancias de Error (o subclases). Los objetos Error incluyen el stack trace, los strings no.


Tipos de Error nativos

JavaScript incluye varios tipos de error específicos:

// TypeError — tipo incorrecto o acceso a null/undefined
null.propiedad       // TypeError
undefined()          // TypeError
1 + null             // no lanza, pero es un bug lógico

// ReferenceError — variable no declarada
console.log(noExiste)  // ReferenceError

// SyntaxError — código malformado (normalmente al parsear)
JSON.parse('{mal json}')  // SyntaxError

// RangeError — valor fuera del rango permitido
new Array(-1)        // RangeError
(1.2345).toFixed(200) // RangeError

// URIError — componente URI malformado
decodeURIComponent('%')  // URIError

// EvalError — relacionado con eval() — muy raro en código moderno

Puedes distinguir el tipo con instanceof:

try {
  //...
} catch (error) {
  if (error instanceof TypeError) {
    console.log('Problema de tipo');
  } else if (error instanceof SyntaxError) {
    console.log('JSON inválido');
  } else {
    throw error;  // re-lanzar errores que no sabemos manejar
  }
}

Errores personalizados

Extiende la clase Error para errors de dominio de tu aplicación:

class ErrorDeValidacion extends Error {
  constructor(campo, mensaje) {
    super(mensaje);          // pasa el mensaje al Error base
    this.name = 'ErrorDeValidacion';
    this.campo = campo;      // propiedad extra
  }
}

class ErrorDeRed extends Error {
  constructor(status, url) {
    super(`HTTP ${status} en ${url}`);
    this.name = 'ErrorDeRed';
    this.status = status;
    this.url = url;
  }
}
function validarEmail(email) {
  if (!email.includes('@')) {
    throw new ErrorDeValidacion('email', 'El email no es válido');
  }
}

try {
  validarEmail('no-es-un-email');
} catch (error) {
  if (error instanceof ErrorDeValidacion) {
    mostrarError(error.campo, error.message);
  } else {
    throw error;  // error inesperado — propagar
  }
}

Manejo de errores en async/await

Hay dos patrones principales:

// Patrón 1: try/catch — controla el flujo explícitamente
async function cargarUsuario(id) {
  try {
    const res = await fetch(`/api/usuarios/${id}`);
    if (!res.ok) throw new ErrorDeRed(res.status, res.url);
    return await res.json();
  } catch (error) {
    if (error instanceof ErrorDeRed && error.status === 404) {
      return null;  // usuario no existe — no es fatal
    }
    throw error;  // otro tipo de error — propagar
  }
}
// Patrón 2: .catch() en el punto de llamada
const usuario = await cargarUsuario(1).catch((error) => {
  console.error('No se pudo cargar:', error.message);
  return null;  // valor por defecto
});

Errores no capturados

Los errores no capturados se pueden escuchar globalmente como último recurso (logging, monitoring):

// Errores síncronos no capturados
window.addEventListener('error', (event) => {
  console.error('Error no capturado:', event.error);
  // Enviar a Sentry, DataDog, etc.
});

// Promesas rechazadas sin .catch()
window.addEventListener('unhandledrejection', (event) => {
  console.error('Promesa rechazada sin capturar:', event.reason);
  event.preventDefault();  // evita que aparezca en la consola del navegador
});

Cuándo lanzar vs. cuándo retornar

No todos los problemas son excepciones. Un patrón útil:

// Lanza: para errores que el llamador debe conocer y manejar
function parsearConfig(json) {
  try {
    return JSON.parse(json);
  } catch {
    throw new Error('Configuración inválida');  // el llamador debe saber
  }
}

// Retorna null/undefined: para ausencias esperadas
async function buscarUsuario(email) {
  const usuario = await bd.findOne({ email });
  return usuario ?? null;  // no es un error que el usuario no exista
}

// Retorna un objeto de resultado: para operaciones donde el fallo es una opción normal
async function intentarLogin(email, pass) {
  const usuario = await buscarUsuario(email);
  if (!usuario) return { ok: false, motivo: 'usuario-no-existe' };
  if (!verificarPassword(pass, usuario.hash)) return { ok: false, motivo: 'password-incorrecto' };
  return { ok: true, usuario };
}

Con esto completamos el Curso de JavaScript Intermedio. Has aprendido a manipular el DOM, gestionar eventos, manejar la asincronía con promesas y async/await, comunicarte con APIs, organizar el código en módulos, extraer datos con desestructuración, entender los closures y el scope, y ahora manejar errores de forma robusta.

El siguiente paso natural es explorar frameworks modernos (React, Vue, Svelte) o profundizar en TypeScript.

Y si quieres una referencia rápida para repasar sintaxis, estructuras de datos y utilidades del lenguaje, tienes la Cheat Sheet completa de JavaScript.