JavaScript

JavaScript

Tutorial JavaScript: Excepciones

Aprende a capturar y gestionar excepciones en JavaScript, incluyendo errores síncronos y asincrónicos con promesas y async/await.

Aprende JavaScript y certifícate

Fundamentos del manejo de excepciones: Captura y lanzamiento de errores en JavaScript

El manejo de excepciones es una técnica fundamental en la programación orientada a objetos que permite controlar situaciones inesperadas o erróneas durante la ejecución del código. En JavaScript, este mecanismo nos ayuda a identificar, capturar y responder a errores de forma estructurada, evitando que nuestra aplicación se detenga abruptamente.

Errores en JavaScript

JavaScript proporciona un sistema de errores basado en objetos que heredan de la clase Error. Cuando ocurre un problema durante la ejecución, el intérprete genera (o "lanza") un objeto de error que contiene información sobre lo sucedido.

Los tipos de errores nativos más comunes son:

  • Error: El tipo base para todos los errores
  • SyntaxError: Errores de sintaxis en el código
  • ReferenceError: Referencias a variables o funciones inexistentes
  • TypeError: Operaciones sobre tipos de datos incorrectos
  • RangeError: Valores fuera del rango permitido

Cada objeto de error contiene propiedades útiles:

try {
  nonExistentFunction();
} catch (error) {
  console.log(error.name);     // "ReferenceError"
  console.log(error.message);  // "nonExistentFunction is not defined"
  console.log(error.stack);    // Traza de la pila de llamadas
}

Estructura try-catch-finally

El bloque try-catch-finally es la estructura básica para manejar excepciones en JavaScript:

try {
  // Código que podría generar un error
} catch (error) {
  // Código que se ejecuta si ocurre un error
} finally {
  // Código que se ejecuta siempre, haya error o no
}

Cada parte cumple una función específica:

  • El bloque try contiene el código que queremos "proteger" y que podría generar errores.
  • El bloque catch se ejecuta solo si ocurre un error en el bloque try.
  • El bloque finally (opcional) se ejecuta siempre, independientemente de si hubo un error o no.

Un ejemplo práctico:

function divideNumbers(a, b) {
  try {
    if (b === 0) {
      throw new Error("Cannot divide by zero");
    }
    
    const result = a / b;
    return result;
  } catch (error) {
    console.error(`Error occurred: ${error.message}`);
    return null;
  } finally {
    console.log("Division operation attempted");
  }
}

console.log(divideNumbers(10, 2));  // 5
console.log(divideNumbers(10, 0));  // null (después de mostrar el error)

Lanzamiento de errores con throw

La palabra clave throw permite generar errores manualmente cuando detectamos condiciones inválidas en nuestro código:

function validateAge(age) {
  if (typeof age !== 'number') {
    throw new TypeError("Age must be a number");
  }
  
  if (age < 0 || age > 120) {
    throw new RangeError("Age must be between 0 and 120");
  }
  
  return true;
}

try {
  validateAge("twenty");  // Lanzará TypeError
} catch (error) {
  console.error(error.name + ": " + error.message);
}

Podemos lanzar cualquier tipo de error nativo o crear nuestros propios tipos personalizados utilizando la herencia de clases que ya conocemos:

// Creamos una clase de error personalizada extendiendo Error
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function processUserData(user) {
  if (!user.name) {
    throw new ValidationError("User name is required");
  }
  
  // Continuar procesando...
}

Este es un excelente ejemplo de cómo el sistema de excepciones se integra con la programación orientada a objetos que hemos estudiado anteriormente.

Captura selectiva de errores

Podemos discriminar entre diferentes tipos de errores para manejarlos de forma específica utilizando instanceof:

try {
  // Código que podría generar diferentes tipos de errores
  const data = JSON.parse(userInput);
  processData(data);
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error("Invalid JSON format");
  } else if (error instanceof ReferenceError) {
    console.error("Missing reference in the data");
  } else {
    console.error("An unexpected error occurred:", error);
  }
}

Propagación de errores

A veces es mejor propagar un error hacia arriba en la pila de llamadas para que sea manejado por un nivel superior:

function validateData(data) {
  if (!data.id) {
    throw new Error("Missing ID field");
  }
  return true;
}

function processUserRecord(record) {
  try {
    validateData(record);
    // Continuar procesando...
  } catch (error) {
    // Añadir contexto al error y propagarlo
    throw new Error(`Error processing user record: ${error.message}`);
  }
}

try {
  processUserRecord({name: "John"});
} catch (error) {
  console.error("Application error:", error.message);
  // Mostrar mensaje amigable al usuario
}

Errores en funciones anidadas

Los errores se propagan automáticamente hacia arriba a través de las llamadas a funciones hasta encontrar un bloque catch:

function innerFunction() {
  throw new Error("Inner error");
}

function middleFunction() {
  innerFunction();  // El error se propaga hacia arriba
}

function outerFunction() {
  try {
    middleFunction();
  } catch (error) {
    console.error("Caught in outer function:", error.message);
  }
}

outerFunction();  // "Caught in outer function: Inner error"

Buenas prácticas básicas

  • Sé específico: Captura solo los errores que puedes manejar adecuadamente.
  • Proporciona mensajes claros: Los mensajes de error deben ser descriptivos y útiles.
  • No abuses de try-catch: Úsalo para condiciones excepcionales, no para el flujo normal del programa.
  • Considera el rendimiento: Los bloques try-catch tienen un pequeño impacto en el rendimiento.
// Enfoque recomendado
function getUserData(userId) {
  if (!userId) {
    throw new Error("User ID is required");
  }
  
  try {
    return fetchUserFromDatabase(userId);
  } catch (error) {
    console.error(`Failed to fetch user ${userId}:`, error.message);
    throw new Error(`Could not retrieve user data: ${error.message}`);
  }
}

Diseño de estrategias robustas para el control de errores: Patrones y mejores prácticas

El manejo de excepciones es más que simplemente usar try-catch. Al integrar estos mecanismos con los principios de POO que hemos aprendido, podemos crear estrategias más robustas para nuestras aplicaciones.

Jerarquía de errores personalizados

Aprovechando la herencia de clases que vimos en el módulo anterior, podemos crear una jerarquía de errores específicos para nuestra aplicación:

// Error base para toda la aplicación
class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    Error.captureStackTrace(this, this.constructor);
  }
}

// Errores específicos por dominio
class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
    this.validationErrors = [];
  }
  
  addValidationError(field, message) {
    this.validationErrors.push({ field, message });
    return this;
  }
}

class AuthorizationError extends AppError {
  constructor(message) {
    super(message, 403);
  }
}

Esta estructura de clases de error permite un manejo contextual más efectivo:

function processUserInput(data) {
  try {
    if (!data.username) {
      const error = new ValidationError("Invalid user data")
        .addValidationError("username", "Username is required");
      throw error;
    }
    
    // Más validaciones...
    
  } catch (error) {
    if (error instanceof ValidationError) {
      // Mostrar errores de validación en la interfaz
      displayValidationErrors(error.validationErrors);
    } else if (error instanceof AuthorizationError) {
      // Redirigir al login
      redirectToLogin();
    } else {
      // Manejar otros tipos de errores
      showGenericError(error.message);
    }
  }
}

Patrón de envoltura de errores

La envoltura de errores preserva el contexto original mientras añade información valiosa en cada nivel:

function processPayment(paymentData) {
  try {
    validatePaymentData(paymentData);
    chargeCustomer(paymentData);
    sendReceipt(paymentData);
  } catch (error) {
    // Envolver el error con contexto adicional
    const wrappedError = new AppError(
      `Payment processing failed: ${error.message}`,
      error.statusCode || 500
    );
    
    // Preservar el error original y añadir contexto
    wrappedError.originalError = error;
    wrappedError.paymentReference = paymentData.reference;
    
    throw wrappedError;
  }
}

Este patrón se alinea bien con el principio de encapsulación que estudiamos en POO, ya que oculta los detalles de implementación mientras proporciona información útil.

Recuperación elegante

La recuperación elegante permite que el sistema continúe funcionando incluso cuando ocurren errores:

function loadUserPreferences(userId) {
  try {
    return fetchUserPreferences(userId);
  } catch (error) {
    // Registrar el error
    console.warn(`Failed to load preferences for user ${userId}:`, error);
    
    // Proporcionar valores predeterminados como fallback
    return getDefaultPreferences();
  }
}

function getDefaultPreferences() {
  return {
    theme: 'light',
    notifications: true,
    language: 'en'
  };
}

Validación preventiva

La validación temprana puede evitar errores antes de que ocurran, siguiendo el principio de "fallar rápido":

function transferFunds(fromAccount, toAccount, amount) {
  // Validación preventiva
  if (typeof amount !== 'number' || amount <= 0) {
    throw new ValidationError("Amount must be a positive number");
  }
  
  if (!fromAccount || !toAccount) {
    throw new ValidationError("Both accounts are required");
  }
  
  // Verificar fondos suficientes antes de intentar la transferencia
  if (fromAccount.balance < amount) {
    throw new AppError("Insufficient funds", 400);
  }
  
  // Proceder con la transferencia...
  fromAccount.balance -= amount;
  toAccount.balance += amount;
  
  return {
    success: true,
    newBalance: fromAccount.balance
  };
}

Contratos y precondiciones

Establecer contratos claros mediante precondiciones ayuda a detectar errores temprano:

function calculateDiscount(product, user) {
  // Precondiciones
  assert(product, "Product is required");
  assert(user, "User is required");
  assert(typeof product.price === 'number', "Product price must be a number");
  
  // Lógica de negocio
  let discount = 0;
  
  if (user.isPremium) {
    discount = product.price * 0.1;
  }
  
  if (product.isOnSale) {
    discount += product.price * 0.05;
  }
  
  // Postcondición
  assert(discount >= 0, "Discount cannot be negative");
  
  return discount;
}

function assert(condition, message) {
  if (!condition) {
    throw new Error(`Assertion failed: ${message}`);
  }
}

Manejo de errores en un contexto orientado a objetos

Podemos integrar el manejo de excepciones directamente en nuestras clases, respetando los principios de POO:

class BankAccount {
  constructor(accountNumber, initialBalance = 0) {
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
    this.transactions = [];
  }
  
  deposit(amount) {
    try {
      if (amount <= 0) {
        throw new ValidationError("Deposit amount must be positive");
      }
      
      this.balance += amount;
      this.recordTransaction("deposit", amount);
      
      return this.balance;
    } catch (error) {
      console.error(`Error in account ${this.accountNumber}:`, error);
      throw error; // Re-lanzar para manejo superior
    }
  }
  
  withdraw(amount) {
    try {
      if (amount <= 0) {
        throw new ValidationError("Withdrawal amount must be positive");
      }
      
      if (amount > this.balance) {
        throw new ValidationError("Insufficient funds");
      }
      
      this.balance -= amount;
      this.recordTransaction("withdrawal", amount);
      
      return this.balance;
    } catch (error) {
      console.error(`Error in account ${this.accountNumber}:`, error);
      throw error;
    }
  }
  
  recordTransaction(type, amount) {
    this.transactions.push({
      type,
      amount,
      date: new Date(),
      balance: this.balance
    });
  }
}

// Uso con manejo de excepciones
try {
  const account = new BankAccount("123456", 1000);
  account.deposit(500);
  account.withdraw(2000); // Esto generará un error
} catch (error) {
  if (error instanceof ValidationError) {
    console.log("Validation error:", error.message);
  } else {
    console.error("Unexpected error:", error);
  }
}

Manejo de excepciones en contextos asíncronos: Promesas, async/await y entornos distribuidos

El código asíncrono plantea desafíos adicionales para el manejo de excepciones. Si bien este tema se explorará en profundidad en el módulo de programación asíncrona, es importante entender los conceptos básicos y cómo se relacionan con lo que hemos aprendido hasta ahora.

Errores en promesas

En JavaScript moderno, las operaciones asíncronas a menudo utilizan promesas, que tienen su propio mecanismo para manejar errores mediante el método .catch():

fetchUserData(userId)
  .then(user => {
    console.log('User data:', user);
  })
  .catch(error => {
    console.error('Failed to fetch user data:', error.message);
  });

Un aspecto importante de las promesas es que los errores se propagan automáticamente a través de la cadena de .then() hasta encontrar un .catch():

fetchUserData(userId)
  .then(user => processUser(user))       // Si fetchUserData falla, este paso se omite
  .then(result => saveToDatabase(result)) // Este también se omite
  .catch(error => {
    // Captura errores de cualquier paso anterior
    console.error('Operation failed:', error);
  });

Introducción a async/await con excepciones

La sintaxis async/await simplifica el manejo de errores asíncronos permitiendo usar bloques try-catch tradicionales:

async function getUserData(userId) {
  try {
    const response = await fetchData(`/users/${userId}`);
    return response;
  } catch (error) {
    console.error(`Failed to fetch user ${userId}:`, error);
    throw new Error(`Could not retrieve user data: ${error.message}`);
  }
}

Es importante recordar que una función async siempre devuelve una promesa, por lo que los errores deben manejarse adecuadamente cuando se llama a estas funciones:

// Opción 1: Usando .catch()
getUserData(123)
  .then(user => displayUserProfile(user))
  .catch(error => showErrorMessage(error.message));

// Opción 2: Usando async/await con try-catch
async function displayUser(userId) {
  try {
    const user = await getUserData(userId);
    displayUserProfile(user);
  } catch (error) {
    showErrorMessage(error.message);
  }
}

Errores comunes en código asíncrono

Uno de los errores más frecuentes es olvidar manejar las promesas rechazadas:

// Incorrecto: error no manejado
function fetchAndProcess(url) {
  fetch(url).then(response => response.json());
  // Si la promesa se rechaza, el error no se maneja
}

// Correcto: con manejo de errores
function fetchAndProcess(url) {
  fetch(url)
    .then(response => response.json())
    .catch(error => console.error("Failed to fetch:", error));
}

Integración de excepciones síncronas y asíncronas

Un desafío común es integrar manejo de errores en código que mezcla operaciones síncronas y asíncronas:

class UserService {
  constructor(apiClient) {
    this.apiClient = apiClient;
    this.cache = new Map();
  }
  
  async getUser(userId) {
    // Verificación síncrona
    if (!userId) {
      throw new ValidationError("User ID is required");
    }
    
    // Verificar caché (síncrono)
    if (this.cache.has(userId)) {
      return this.cache.get(userId);
    }
    
    // Operación asíncrona
    try {
      const user = await this.apiClient.fetchUser(userId);
      
      // Más validaciones síncronas
      if (!user.name) {
        throw new ValidationError("Invalid user data received");
      }
      
      // Guardar en caché
      this.cache.set(userId, user);
      return user;
    } catch (error) {
      // Transformar errores específicos de la API
      if (error.status === 404) {
        throw new NotFoundError(`User ${userId} not found`);
      }
      // Re-lanzar otros errores
      throw error;
    }
  }
}

Un vistazo a patrones asíncronos más avanzados

En el módulo de programación asíncrona exploraremos estos conceptos con mayor profundidad, incluyendo:

  • Manejo de múltiples operaciones asíncronas con Promise.all() y Promise.allSettled()
  • Implementación de timeouts para operaciones asíncronas
  • Estrategias de reintentos para operaciones que pueden fallar temporalmente
  • Manejo de errores en APIs basadas en eventos
  • Patrones de recuperación en sistemas distribuidos

Por ahora, es importante entender que los principios fundamentales de manejo de excepciones se aplican tanto en código síncrono como asíncrono, aunque con algunas consideraciones adicionales.

Aprende JavaScript online

Otras lecciones de JavaScript

Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a JavaScript y certifícate

Ejercicios de programación de JavaScript

Evalúa tus conocimientos de esta lección Excepciones con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.