Skip to content

Errores y depuración ​

Todo programador escribe bugs; los buenos programadores los encuentran rápido y escriben código que falla con elegancia. En esta lección aprenderás a manejar errores con try/catch, crear tus propios tipos de error, capturar fallos asíncronos y usar las DevTools del navegador para depurar como profesional, en lugar de llenar tu código de console.log.

Concepto ​

Un error es como la luz de advertencia del tablero de un auto: ignorarla no la hace desaparecer. Tienes dos trabajos:

  1. Manejar los errores esperables (la red falla, el usuario escribe mal un dato, la API responde 404) para que la app siga funcionando y el usuario sepa qué pasó.
  2. Encontrar y corregir los errores de programación (bugs) con herramientas de depuración.

Cuando ocurre un error que nadie captura, JavaScript detiene la ejecución de ese bloque de código y lo muestra en rojo en la consola.

Leer un mensaje de error ​

txt
Uncaught TypeError: Cannot read properties of undefined (reading 'nombre')
    at mostrarUsuario (app.js:12:24)
    at HTMLButtonElement.<anonymous> (app.js:30:3)
  • Tipo: TypeError.
  • Mensaje: intentaste leer nombre de algo que es undefined.
  • Stack trace (pila): dónde ocurrió (app.js, línea 12, columna 24) y quién llamó a esa función. Haz clic en el enlace de la consola para ir directo a la línea.

Buena práctica

Lee el error completo antes de buscar en Google. El 80 % de las veces el mensaje y la línea te dicen exactamente qué pasa.

Tipos de errores nativos ​

TipoCuándo ocurreEjemplo
SyntaxErrorCódigo mal escrito (no se ejecuta nada)Falta un paréntesis, JSON.parse('{mal}')
ReferenceErrorVariable que no existeconsole.log(totl)
TypeErrorOperación sobre un tipo incorrectoundefined.nombre, null()
RangeErrorValor fuera de rangonew Array(-1), recursión infinita
URIErrorURI mal formadadecodeURIComponent('%')

try, catch y finally ​

js
try {
  // Código que puede fallar
  const datos = JSON.parse(textoDelServidor);
  procesar(datos);
} catch (error) {
  // Se ejecuta SOLO si algo falló en el try
  console.error('No se pudieron procesar los datos:', error.message);
  mostrarMensaje('Hubo un problema al cargar la información.');
} finally {
  // Se ejecuta SIEMPRE (haya error o no): limpieza
  ocultarSpinner();
}

El objeto error tiene:

js
error.name;     // 'SyntaxError'
error.message;  // 'Unexpected token m in JSON at position 1'
error.stack;    // la pila completa como texto
error.cause;    // la causa original (si se indicó)

Si no necesitas el error, puedes omitir el parámetro: catch { ... }.

Error común: tragarse los errores

js
try {
  guardarPedido(pedido);
} catch (e) {
  // vacío 🙈
}

Un catch vacío oculta los bugs: la app falla en silencio y nadie sabe por qué. Como mínimo, registra el error con console.error y avisa al usuario.

Lanzar errores con throw ​

Tú también puedes lanzar errores cuando algo no es válido. Así detienes el flujo en cuanto detectas un problema:

js
function calcularDescuento(precio, porcentaje) {
  if (typeof precio !== 'number' || precio < 0) {
    throw new Error('El precio debe ser un número positivo');
  }
  if (porcentaje < 0 || porcentaje > 100) {
    throw new RangeError(`Porcentaje inválido: ${porcentaje}`);
  }
  return precio * (1 - porcentaje / 100);
}

try {
  calcularDescuento(500, 150);
} catch (error) {
  console.log(error.name, error.message); // 'RangeError' 'Porcentaje inválido: 150'
}

Error común

Lanza siempre objetos Error (o subclases), no strings: throw 'falló' no tiene stack ni name, y dificulta la depuración. Usa throw new Error('falló').

Encadenar la causa ​

js
async function cargarPerfil(id) {
  try {
    return await obtenerUsuario(id);
  } catch (error) {
    // Añade contexto sin perder el error original
    throw new Error(`No se pudo cargar el perfil ${id}`, { cause: error });
  }
}

Errores personalizados ​

Crear tus propias clases de error te permite distinguir qué tipo de problema ocurrió y reaccionar diferente a cada uno:

js
class AppError extends Error {
  constructor(mensaje, opciones) {
    super(mensaje, opciones);
    this.name = this.constructor.name;
  }
}

class ValidacionError extends AppError {
  constructor(campo, mensaje) {
    super(mensaje);
    this.campo = campo;
  }
}

class NoEncontradoError extends AppError {}

class RedError extends AppError {}

// Uso
function validarProducto(p) {
  if (!p.nombre) throw new ValidacionError('nombre', 'El nombre es obligatorio');
  if (p.precio <= 0) throw new ValidacionError('precio', 'El precio debe ser mayor a 0');
}

try {
  validarProducto({ nombre: 'Mouse', precio: 0 });
} catch (error) {
  if (error instanceof ValidacionError) {
    marcarCampoConError(error.campo, error.message);
  } else if (error instanceof RedError) {
    mostrarToast('Revisa tu conexión a internet');
  } else {
    throw error; // un error que no sé manejar: lo dejo subir
  }
}

Buena práctica

Captura sólo los errores que sabes manejar y relanza el resto. Un catch que lo atrapa todo y sigue como si nada puede esconder bugs graves.

Errores en código asíncrono ​

js
// Con async/await: try/catch normal
async function cargar() {
  try {
    const r = await fetch('/api/productos');
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return await r.json();
  } catch (error) {
    console.error(error);
    return [];
  }
}

// Con promesas: .catch()
fetch('/api/productos')
  .then((r) => r.json())
  .catch((error) => console.error(error));

Error común: try/catch no atrapa callbacks

js
try {
  setTimeout(() => {
    throw new Error('¡Boom!'); // NO lo atrapa el catch de afuera
  }, 1000);
} catch (e) {
  console.log('Nunca llega aquí');
}

Cuando el callback se ejecuta, el try ya terminó hace rato. El manejo de errores debe ir dentro del callback, o usar promesas con await.

También debes await-ear las promesas dentro del try:

js
async function guardar() {
  try {
    return enviarDatos(); // ❌ sin await: si la promesa falla, el catch no se entera
  } catch (e) { /* ... */ }
}

Red de seguridad global ​

Para errores que se escapan de todos tus try/catch, registra manejadores globales. En producción se usan para enviar los errores a servicios como Sentry:

js
// Errores síncronos no capturados
window.addEventListener('error', (evento) => {
  console.error('Error global:', evento.message, evento.filename, evento.lineno);
  // enviarAlServidor({ mensaje: evento.message, stack: evento.error?.stack });
});

// Promesas rechazadas sin .catch()
window.addEventListener('unhandledrejection', (evento) => {
  console.error('Promesa sin manejar:', evento.reason);
});

Depuración con la consola ​

js
console.log('Valor:', valor);
console.info('Información');
console.warn('Advertencia');           // amarillo
console.error('Error');                // rojo, con stack

console.table(productos);              // arrays de objetos como tabla
console.dir(document.body);            // ver un elemento como objeto
console.log({ usuario, total });       // truco: muestra nombre y valor

console.group('Carrito');              // agrupar mensajes
console.log('Items:', items.length);
console.log('Total:', total);
console.groupEnd();

console.time('render');                // medir rendimiento
renderizarLista();
console.timeEnd('render');             // render: 12.4 ms

console.count('clic');                 // cuántas veces pasó por aquí
console.assert(total >= 0, 'El total no puede ser negativo'); // sólo imprime si falla
console.trace('¿Quién llamó a esta función?');

Depuración con breakpoints ​

Los console.log sirven, pero un breakpoint es mucho más poderoso: pausa el programa en una línea y te deja inspeccionar todas las variables en ese momento.

Paso a paso en Chrome / Edge / Firefox ​

  1. Abre DevTools (F12 o Ctrl + Shift + I; en Mac Cmd + Option + I).
  2. Ve a la pestaña Sources (Fuentes) y abre tu archivo con Ctrl + P.
  3. Haz clic en el número de línea: aparece un marcador azul (breakpoint).
  4. Provoca la acción (clic en el botón). El código se pausa en esa línea.
  5. Observa:
    • Scope: todas las variables y sus valores actuales.
    • Call Stack: qué funciones llamaron a la actual.
    • Watch: expresiones que quieras vigilar (total * 1.16).
    • Pasa el mouse sobre cualquier variable del código para ver su valor.
  6. Controla la ejecución:
    • Resume (F8): continuar hasta el siguiente breakpoint.
    • Step over (F10): ejecutar la línea actual y pasar a la siguiente.
    • Step into (F11): entrar a la función que se está llamando.
    • Step out (Shift + F11): terminar la función actual y volver a quien la llamó.

La palabra clave debugger ​

Escribir debugger; en tu código actúa como un breakpoint cuando las DevTools están abiertas:

js
function calcularTotal(items) {
  let total = 0;
  for (const item of items) {
    debugger; // se pausa aquí en cada vuelta
    total += item.precio * item.cantidad;
  }
  return total;
}

Importante

Nunca subas a producción código con debugger ni con console.log de depuración. Configura ESLint para que te avise (no-debugger, no-console).

Breakpoints especiales ​

  • Condicionales: clic derecho en la línea → Add conditional breakpoint → item.precio < 0. Sólo se pausa cuando la condición es verdadera.
  • Logpoints: clic derecho → Add logpoint: imprime en consola sin modificar tu código.
  • En eventos: panel Event Listener Breakpoints → Mouse > click. Se pausa en cualquier clic.
  • En cambios del DOM: en la pestaña Elements, clic derecho en un nodo → Break on → subtree modifications. Útil para descubrir "¿qué código está cambiando este elemento?".
  • En excepciones: activa Pause on exceptions para detenerte justo donde se lanza un error.

La pestaña Network ​

Indispensable cuando trabajas con APIs:

  • Lista cada petición: URL, método, código de estado, tamaño y tiempo.
  • Haz clic en una para ver Headers (lo que enviaste), Payload (el cuerpo), Preview/Response (lo que respondió el servidor).
  • Filtra por Fetch/XHR para ver sólo las llamadas a APIs.
  • Usa Throttling (Slow 3G) para probar cómo se ve tu estado de "cargando" con una conexión lenta.
  • Marca Offline para probar tu manejo de errores de red.
  • Clic derecho → Copy as fetch o Copy as cURL para reproducir la petición.

Método para cazar bugs ​

  1. Reprodúcelo: encuentra los pasos exactos que lo provocan.
  2. Lee el error y ve a la línea indicada.
  3. Formula una hipótesis: "creo que usuario es undefined porque la API tarda".
  4. Comprueba con un breakpoint o un console.log estratégico.
  5. Aísla: comenta código o reduce el caso al mínimo hasta encontrar la causa.
  6. Corrige la causa, no el síntoma. Y prueba de nuevo.
  7. Explícalo en voz alta (la técnica del "pato de hule"): describir el código línea por línea suele revelar el error.

Ejercicios ​

  1. Escribe parsearJSON(texto) que devuelva { ok: true, datos } o { ok: false, error: mensaje } sin lanzar nunca.
  2. Crea una función dividir(a, b) que lance un RangeError si b es 0 y un TypeError si alguno no es número. Pruébala con try/catch mostrando error.name.
  3. Crea la clase SaldoInsuficienteError con propiedades saldo y monto, y úsala en una función retirar.
  4. Encuentra el bug (pista: pon un breakpoint dentro del bucle):
    js
    function promedio(nums) {
      let suma = 0;
      for (let i = 0; i <= nums.length; i++) suma += nums[i];
      return suma / nums.length;
    }
    promedio([8, 9, 10]); // NaN
  5. Registra un manejador global que muestre un aviso discreto en pantalla cuando haya una promesa rechazada sin manejar.
Ver solución
js
// 1
function parsearJSON(texto) {
  try {
    return { ok: true, datos: JSON.parse(texto) };
  } catch (error) {
    return { ok: false, error: error.message };
  }
}

// 2
function dividir(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') throw new TypeError('Ambos deben ser números');
  if (b === 0) throw new RangeError('No se puede dividir entre 0');
  return a / b;
}
try {
  dividir(10, 0);
} catch (error) {
  console.log(error.name); // 'RangeError'
}

// 3
class SaldoInsuficienteError extends Error {
  constructor(saldo, monto) {
    super(`Saldo insuficiente: tienes $${saldo} y quieres retirar $${monto}`);
    this.name = 'SaldoInsuficienteError';
    this.saldo = saldo;
    this.monto = monto;
  }
}
function retirar(saldo, monto) {
  if (monto > saldo) throw new SaldoInsuficienteError(saldo, monto);
  return saldo - monto;
}

// 4: el bucle usa <= y en la última vuelta nums[3] es undefined → 27 + undefined = NaN.
function promedio(nums) {
  let suma = 0;
  for (let i = 0; i < nums.length; i++) suma += nums[i];
  return suma / nums.length;
}

// 5
window.addEventListener('unhandledrejection', (e) => {
  const aviso = document.createElement('div');
  aviso.textContent = 'Algo salió mal. Intenta de nuevo.';
  aviso.style.cssText = 'position:fixed;bottom:16px;right:16px;background:#991b1b;color:#fff;padding:10px 14px;border-radius:8px';
  document.body.append(aviso);
  setTimeout(() => aviso.remove(), 4000);
  console.error(e.reason);
});

Resumen ​

  • Lee el error completo: tipo, mensaje y stack trace te dicen qué y dónde.
  • try/catch/finally maneja errores; throw new Error() los lanza. Nunca dejes un catch vacío.
  • Crea errores personalizados con extends Error para reaccionar según el tipo; relanza lo que no sepas manejar.
  • En código asíncrono usa await dentro del try o .catch(); registra error y unhandledrejection como red de seguridad.
  • Depura con breakpoints, debugger, la consola avanzada y la pestaña Network.

Siguiente: Buenas prácticas

Academia Web — aprende, copia y construye.