Ir al contenido
wannacode

Lanzar excepciones y crear las tuyas

En resumen: Con throw una función se niega y lanza un error que cae a través de las llamadas hasta que algún catch lo atrapa; para tu propio caso escribes tu propia clase de error.

Problema

El cajero dice «recoge tu dinero»

La cuenta guarda su saldo. Si el importe a retirar es negativo o no hay dinero suficiente, el método simplemente no hace nada:

withdraw(amount) {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
}
}

El cajero lo usa así:

const account = new Account(100);
account.withdraw(500);
console.log("Recoge tu dinero.");
console.log(`Saldo: ${account.balance} €`);
Salida
Recoge tu dinero.
Saldo: 100 €

El cliente pide 500 € y tiene 100 €. El método se niega en silencio, el cajero no se da cuenta y dice «Recoge tu dinero.». El error se pierde. El método podría devolver true o false, pero quien lo llama puede olvidarse fácilmente de comprobarlo, y false no dice por qué.

La idea

throw: me niego y digo por qué

En lugar de callarse, el método lanza una excepción:

withdraw(amount) {
if (amount <= 0) {
throw new RangeError("El importe debe ser positivo");
}
if (amount > this.balance) {
throw new RangeError("No alcanza el dinero");
}
this.balance -= amount;
}

throw termina el método como return. A la línea this.balance -= amount nunca se llega. Pero en lugar de un valor, el método envía una excepción, y quien lo llama no puede pasarla por alto:

const account = new Account(100);
try {
account.withdraw(500);
console.log("Recoge tu dinero.");
} catch (e) {
console.log(`Rechazado: ${e.message}`);
}
console.log(`Saldo: ${account.balance} €`);
Salida
Rechazado: No alcanza el dinero
Saldo: 100 €
  • new crea el objeto de la excepción, como cualquier objeto. El mensaje va a su constructor, y e.message lo devuelve.
  • RangeError es la excepción ya hecha para «el valor está fuera del rango». El importe aquí es justo eso: por debajo de cero o por encima del saldo.
  • catch (e) en JavaScript no tiene tipo entre los paréntesis y atrapa cualquier excepción. Aquí solo hay una excepción, así que basta.
Más precisamente, para curiosos
  • Otra excepción ya hecha muy común es TypeError: el valor es de un tipo incorrecto. Para todo lo demás usas Error o tu propia excepción.
  • throw en JavaScript acepta cualquier valor, incluso un string: throw "No alcanza el dinero" funciona. Pero entonces en catch no hay e.message ni e.stack (los dos son undefined), y no hay stack trace. Así que lanza solo Error o una clase hija suya.

Míralo

La excepción cae

withdraw(amount) {
if (amount > this.balance) {
throw new RangeError("No alcanza el dinero");
}
this.balance -= amount;
}
function payRent(account) {
account.withdraw(800);
console.log("El alquiler está pagado.");
}
try {
payRent(new Account(500));
} catch (e) {
console.log(`Rechazado: ${e.message}`);
}
Paso a paso, en palabras
  1. El programa llama a payRent para una cuenta con 500 €.
  2. payRent llama a withdraw(800): un marco nuevo encima.
  3. 800 > 500: el método lanza una excepción.
  4. El saldo no cambia: el marco se descarta sin terminar sus líneas.
  5. payRent no tiene catch: «El alquiler está pagado.» no se imprime, el marco se descarta.
  6. catch en el programa atrapa la excepción y este sigue desde aquí.

Recuerda la pila de llamadas de la lección de recursión. Abajo está el programa: el código del nivel superior. Llama a payRent, y esta llama a withdraw. Los marcos están uno encima de otro.

Cuando withdraw lanza, la excepción busca un catch bajando por la pila. Un marco sin un catch que coincida se descarta enseguida, sin terminar sus líneas. Eso es lo que le pasa también a payRent. Solo el programa tiene un catch, y sigue desde ahí.

Si tampoco hubiera catch ahí, el programa se detendría e imprimiría un stack trace: justo estos marcos.

Más precisamente, para curiosos

Aquí está el mismo programa, pero nadie atrapa (el archivo rent.js):

class Account {
constructor(balance) {
this.balance = balance;
}
withdraw(amount) {
if (amount > this.balance) {
throw new RangeError("No alcanza el dinero");
}
this.balance -= amount;
}
}
function payRent(account) {
account.withdraw(800);
console.log("El alquiler está pagado.");
}
payRent(new Account(500));
Error
/projects/rent/rent.js:8
throw new RangeError("No alcanza el dinero");
^
RangeError: No alcanza el dinero
at Account.withdraw (/projects/rent/rent.js:8:13)
at payRent (/projects/rent/rent.js:15:11)
at Object.<anonymous> (/projects/rent/rent.js:19:1)
at Module._compile (node:internal/modules/cjs/loader:1705:14)
at Object..js (node:internal/modules/cjs/loader:1838:10)
at Module.load (node:internal/modules/cjs/loader:1441:32)
at Function._load (node:internal/modules/cjs/loader:1263:12)
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:237:24)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
Node.js v22.22.2

Node muestra la línea donde se lanzó la excepción, después el tipo y el mensaje, y después los marcos de dentro hacia fuera, como Java: Account.withdraw, payRent, y Object.<anonymous> es el programa. Las líneas con node:internal son el código del propio Node.

Pruébalo

¿Qué llega a la pantalla?

La cuenta tiene 50 €, y el pedido es de 120 €.

¿Qué imprimirá el programa?

function checkout(account) {
console.log("Pago el pedido");
account.withdraw(120);
console.log("El pedido está pagado");
}
const account = new Account(50);
try {
checkout(account);
console.log("Envío la confirmación");
} catch (e) {
console.log(`Rechazado: ${e.message}`);
}
console.log(`Saldo: ${account.balance} €`);
Respuesta

Pruébalo

Encuentra el error

El alquiler es de 800 €, y la cuenta tiene 500 €. El programa dice «El alquiler está pagado.». ¿En qué línea está el error?

Elige la línea con el error.

Más precisamente, para curiosos
  • A veces una función atrapa, hace algo, como escribir en un registro, y vuelve a lanzar la misma excepción. En JavaScript eso es throw e; en catch. El stack trace sigue siendo el original: se registra cuando se crea el Error, no cuando se lanza.
function payRent(account) {
try {
account.withdraw(800);
} catch (e) {
console.log("Registro: el alquiler no se pagó");
throw e;
}
}
try {
payRent(new Account(500));
} catch (e) {
console.log(`Rechazado: ${e.message}`);
}
Salida
Registro: el alquiler no se pagó
Rechazado: No alcanza el dinero
  • También puedes lanzar otra excepción que diga mejor qué pasó, y guardar la antigua como causa. En JavaScript: new Error("El alquiler no está pagado", { cause: e }). La antigua está en e.cause, y Node la imprime debajo de la nueva como [cause].
function payRent(account) {
try {
account.withdraw(800);
} catch (e) {
throw new Error("El alquiler no está pagado", { cause: e });
}
}
try {
payRent(new Account(500));
} catch (e) {
console.log(`${e.message}, porque: ${e.cause.message}`);
}
Salida
El alquiler no está pagado, porque: No alcanza el dinero

La idea

Tu propia excepción

«No alcanza el dinero» es texto. Si el cajero quiere proponer «Ingresa 30 € más», tiene que saber cuánto falta. Sacar el número del texto es frágil. Mejor haz tu propia clase de excepción con un campo:

class InsufficientFundsError extends Error {
constructor(shortfall) {
super(`Faltan ${shortfall} €`);
this.name = "InsufficientFundsError";
this.shortfall = shortfall;
}
}

Tu propia excepción es una clase normal que hereda de Error. El constructor pasa el mensaje a la clase padre con super(...). this.name le da nombre a la excepción: sin él, e.name y el stack trace dicen solo Error. El método la lanza cuando no alcanza el dinero:

withdraw(amount) {
if (amount <= 0) {
throw new RangeError("El importe debe ser positivo");
}
if (amount > this.balance) {
throw new InsufficientFundsError(amount - this.balance);
}
this.balance -= amount;
}

Y quien lo llama atrapa los dos casos por separado:

const account = new Account(100);
try {
account.withdraw(130);
} catch (e) {
if (e instanceof InsufficientFundsError) {
console.log(e.message);
console.log(`Ingresa ${e.shortfall} € más o elige un importe menor.`);
} else if (e instanceof RangeError) {
console.log(`Rechazado: ${e.message}`);
} else {
throw e;
}
}
Salida
Faltan 30 €
Ingresa 30 € más o elige un importe menor.

catch en JavaScript es uno solo, y lo atrapa todo. Dentro compruebas el tipo con instanceof, y una excepción que no esperas la vuelves a lanzar con throw e;. Si no, el else vacío se la tragaría, como en la tarea de hace un momento.

Más precisamente, para curiosos
  • Los nombres de las excepciones siempre terminan en Error.
  • Tu propia excepción también puede heredar de una clase ya hecha más concreta, como RangeError. Entonces e instanceof RangeError también es verdadero para ella. Aquí es aparte, así que el orden de las dos comprobaciones no importa.

La idea

¿Propia o ya hecha?

Aquí no hay excepciones comprobadas como en Java. Nadie comprueba antes de que arranque el programa si quien llama atrapa. Lo que lanza una función está escrito en su documentación: un comentario JSDoc con @throws.

/**
* Retira el importe de la cuenta.
* @throws {RangeError} si el importe no es positivo
* @throws {InsufficientFundsError} si no alcanza el dinero
*/
withdraw(amount) {
if (amount <= 0) {
throw new RangeError("El importe debe ser positivo");
}
if (amount > this.balance) {
throw new InsufficientFundsError(amount - this.balance);
}
this.balance -= amount;
}

Esto es solo una costumbre, para las personas y para el editor. Un programa que llama a withdraw sin try arranca y se ejecuta hasta que la excepción de verdad se lanza. La pregunta es otra: ¿necesitas una excepción propia?

Una excepción ya hecha, cuando el significado coincide: RangeError para un valor incorrecto, TypeError para un tipo incorrecto. Una propia, cuando quien llama tiene que distinguirla de las demás y reaccionar a su manera, cuando la excepción tiene que llevar datos, como shortfall, o cuando no hay ninguna ya hecha con ese significado, como para retirar de una cuenta cerrada.

Pruébalo

Rellena el hueco

Una excepción para una edad no válida. ¿Qué va en el hueco para que el mensaje llegue a la clase padre?

class InvalidAgeError extends Error {
  constructor(message) {
    (message);
    this.name = "InvalidAgeError";
  }
}

Ejercicio

Excepciones en clases

Resuélvelas en tu editor.

1. Tienda de música. La tienda toca instrumentos en una demostración, pero no todos se pueden tocar.

  • Una clase MusicalInstrument con un nombre, una marca y un precio, y un constructor.
  • Guitar y Piano heredan del instrumento y tienen un método play(). Drum solo hereda. JavaScript no tiene interfaces: «puede tocar» significa «tiene un método play».
  • class NotPlayableError extends Error.
  • MusicShop con un método demoInstrument(instrument): si el instrumento puede tocar (typeof instrument.play === "function"), lo toca; si no, lanza NotPlayableError.
  • En el programa, haz la demostración de una guitarra Fender Stratocaster (1299 €), una batería Pearl Export (849 €) y un piano Yamaha P-45 (499 €) desde un array. Atrapa la excepción de cada instrumento por separado, para que la demostración siga.
Salida
Fender Stratocaster, 1299.00 €
La guitarra toca un acorde.
Pearl Export, 849.00 €
Pearl Export no se puede usar en una demostración.
Yamaha P-45, 499.00 €
El piano toca una escala.

2. Una cuenta con reglas. Añade a Account un método deposit(amount) que lance RangeError con un importe de 0 o menos. Retirar lanza InsufficientFundsError de la lección. La cuenta empieza con 100 €. Lee el número de operaciones y después las propias operaciones, cada una en su propia línea: «ingresa 50» o «retira 500». Después de cada una, imprime el saldo o por qué se rechazó la operación.

Entrada
4
ingresa 50
retira 500
retira -5
retira 120
Salida
Saldo: 150 €
Faltan 350 €. Saldo: 150 €
El importe debe ser positivo. Saldo: 150 €
Saldo: 30 €

3. Edad no válida. Person tiene un nombre y una edad. El setter set age(value) lanza InvalidAgeError de la tarjeta anterior si la edad es menor que 0 o mayor que 150. El constructor también pasa por this.age = age, para que nadie pueda crear una persona con una edad no válida. Crea a María (19), Pablo (−3) y Juan (200).

Salida
María, 19 años
Pablo: Edad no válida: -3
Juan: Edad no válida: 200

Más tareas

  • Reserva de mesa. Una clase Restaurant con un número de mesas libres y un método para reservar una. Si se han acabado las mesas, lanza tu propia NoTablesError. Si el número de invitados es 0 o menos, lanza la excepción ya hecha para un argumento incorrecto.
  • Un producto con existencias. Una clase Product con una cantidad en existencias y un método sell(qty). Si no hay suficiente en existencias, lanza OutOfStockError, que lleva cuántas unidades hay. La tienda atrapa la excepción y ofrece vender las que hay en existencias.
  • Tienda de música con motivo. Añade a NotPlayableError un campo con el instrumento que no puede tocar, e imprime su precio en catch.

Test

Ponte a prueba

1. ¿Qué hace throw new RangeError("...")?
2. El programa llama a a, a llama a b, b lanza una excepción. Solo el código del nivel superior tiene un catch. ¿Qué pasa con las líneas de a después de la llamada a b?
3. ¿Dónde es mejor atrapar la excepción?
4. ¿Por qué el constructor de InsufficientFundsError pone this.name = "InsufficientFundsError"?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·