Gestión de Errores#

En JavaScript, se pueden capturar y manejar errores para asegurar que el código continúe funcionando adecuadamente, incluso cuando algo falla.

Cuando algo falla en JavaScript, el hilo de ejecución se detiene y deja de funcionar. Para evitar que esto ocurra, es posible capturar y manejar errores conocidos. Los errores no capturados paran el hilo en el que están. Puede ser el hilo principal o la ejecución de callbacks. Depende dónde se produzca el error puede ser maś o menos catastrófico y dejar la web inutilizable.

Los errores pueden ser por errores, malas prácticas o simplemente porque falla algo sobre lo que no tenemos control como la red.

Objeto Error#

Cuando algo falla, se lanza un objeto Error. Este objeto contiene propiedades específicas como message, name, fileName, lineNumber, columnNumber, y stack. Además, tiene un método útil llamado toString().

Aunque Error es genérico, también existen errores más específicos como EvalError, InternalError, SyntaxError, RangeError, TypeError y URIError.

Ese error se puede lanzar con throw y capturar con un bloque try...catch. Si es tratado por el catch, el error no pasa de ahí y no corta la ejecución del hilo. Además, se puede aprovechar el catch para solucionar el problema que lo ha causado.

try {
    throw new Error('¡Ups!')
} catch (e) {
    console.error(e.name + ': ' + e.message)
}
Error: ¡Ups!

Manejar Errores#

Los errores pueden ser manejados de forma genérica o específica usando instanceof. Además, es posible crear errores personalizados. Si es necesario ejecutar código independientemente de si ocurre un error, se utiliza finally().

try {
    foo.bar();
} catch (e) {
    console.error(e.toString());
    if (e instanceof EvalError) {
        console.error(e.name + ': ' + e.message);
    } else if (e instanceof RangeError) {
        console.error(e.name + ': ' + e.message);
    } // ... etc
} finally {
    closeMyFile();
}
ReferenceError: foo is not defined
Stack trace:
ReferenceError: closeMyFile is not defined
    at <anonymous>:11:3

En ocasiones queremos tratar el error en una función pero que siga escalando para que lo trate una función superior. Esto es hacer un rethrow y consiste en poner un throw dentro del catch:

async function fetchUser(id) {
    const res = await fetch(`/users/${id}`);
    if (!res.ok) throw new Error("User not found");
    return res.json();
  }
async function getUser(id) {
    try {
      const user = await fetchUser(id);
      return user;
    } catch (err) {
      console.error("Fetching user failed:", err.message);
      throw new Error("Unable to load user profile"); 
    }
  }

Es posible lanzar errores personalizados utilizando el constructor de Error.

class ValidationError extends Error {
  constructor(field, message) {
    super(`${field}: ${message}`);
    this.name = "ValidationError";
    this.field = field;
  }
}

function validateUser(user) {
  if (!user.email.includes("@")) {
    throw new ValidationError("email", "Invalid email format");
  }
  if (!user.age || user.age < 18) {
    throw new ValidationError("age", "User must be 18+");
  }
}

Errores en Promesas#

Cuando una promesa no puede ser cumplida, se ejecuta la función reject(), la cual puede ser capturada en el bloque .catch(). Sin embargo, los errores en promesas no pueden ser capturados directamente con try...catch debido a que .catch() siempre retorna otra promesa.

Por tanto el .catch() de las promesas no es igual y puede recibir tanto un Error como cualquier otra cosa que se pase a reject. De todas formas, un throw dentro de un .then() o .catch() provoca que retorne una promesa fallida que puede ser capturada por .catch(). Como se ve en el ejemplo:

let promesa3 = new Promise(function promesa(resolve, reject) {
    try { 
        if (Math.random() > 0.5) resolve('Funciona 3');
        else throw new Error('No funciona 3');
    } catch (error) {
        reject(error.message);
    }
});

promesa3.then(function r(message) {
    console.log(message);
}).catch(function c(error) {
    throw new Error(error);
}).catch(function c(error) {
    console.error(error.toString());
});
Error: No funciona 3
Promise { undefined }

En el interior de cualquier función ejecutora de promesas o de callback de .then() o .catch() se puede usar el bloque tradicional try...catch siempre que se use reject o throw en cada caso.

En ocasiones se puede decidir trabajar sólo con la sintaxis de las promesas o con el método tradicional.

Errores en Async/Await#

Cuando se utiliza async/await, se pueden manejar errores dentro de un bloque try...catch. Si una promesa es rechazada (reject), await lanza un error que puede ser capturado con catch.

async function getData(url) {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`Error de servidor: ${response.status} ${response.statusText}`);
        }
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error:', error.message);
        throw error;
    }
}

async function fetchExample() {
    const url = 'https://api.foo.com/data';
    try {
        const data = await getData(url);
        console.log('Datos recibidos:', data);
    } catch (error) {
        console.error('Error en la solicitud:', error);
    }
}

fetchExample();
Promise { <pending> }

En código moderno, se recomienda usar async/await con try...catch por su legibilidad. Frente a la sintaxis de las promesas.

Errores en Observables#

Los Observables pueden manejar errores ejecutando la función error() del Observer. Además, pueden retornar un error usando throwError() y este error puede ser capturado con catchError() dentro de las pipes.

import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

const observable = new Observable(subscriber => {
    try {
        subscriber.next('Next value');
        throw new Error('Something went wrong');
    } catch (error) {
        subscriber.error(error);
    }
});

observable.pipe(
    catchError(error => {
        console.error('Caught error:', error);
        return throwError(error); // Propaga el error
    })
).subscribe({
    next(value) { console.log(value); },
    error(err) { console.error('Error handler:', err); }
});

En caso de que un Observable falle, es posible reintentar una cantidad limitada de veces utilizando retry().

import { retry } from 'rxjs/operators';

observable.pipe(
    retry(3),
    catchError(error => {
        console.error('Caught error after retries:', error);
        return throwError(error);
    })
).subscribe({
    next(value) { console.log(value); },
    error(err) { console.error('Error handler:', err); }
});