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} €`);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} €`);Rechazado: No alcanza el dineroSaldo: 100 €newcrea el objeto de la excepción, como cualquier objeto. El mensaje va a su constructor, ye.messagelo devuelve.RangeErrores 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 usasErroro tu propia excepción. throwen JavaScript acepta cualquier valor, incluso un string:throw "No alcanza el dinero"funciona. Pero entonces encatchno haye.messagenie.stack(los dos sonundefined), y no hay stack trace. Así que lanza soloErroro 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
- El programa llama a payRent para una cuenta con 500 €.
- payRent llama a withdraw(800): un marco nuevo encima.
- 800 > 500: el método lanza una excepción.
- El saldo no cambia: el marco se descarta sin terminar sus líneas.
- payRent no tiene catch: «El alquiler está pagado.» no se imprime, el marco se descarta.
- 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));/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.2Node 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} €`);«Pago el pedido» va antes del retiro. Después withdraw lanza, y el marco de checkout se descarta: «El pedido está pagado» nunca aparece. En el programa, «Envío la confirmación» también se salta, porque está en try después de la llamada. catch imprime el rechazo, y el saldo queda intacto.
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.
catch vacío atrapa la excepción y no hace nada. El error desaparece, igual que con el cajero de la primera tarjeta. Se atrapa donde se puede hacer algo sensato. Aquí payRent no puede encontrar el dinero, así que no debería atrapar. Quita el try/catch: la excepción llegará al programa, y este avisará a la persona.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;encatch. El stack trace sigue siendo el original: se registra cuando se crea elError, 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}`);}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á ene.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}`);}El alquiler no está pagado, porque: No alcanza el dineroLa 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; }}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. Entoncese instanceof RangeErrortambié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";
}
}super(message) llama al constructor de la clase padre. La clase padre guarda el mensaje, y e.message lo devuelve.
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
MusicalInstrumentcon un nombre, una marca y un precio, y un constructor. GuitaryPianoheredan del instrumento y tienen un métodoplay().Drumsolo hereda. JavaScript no tiene interfaces: «puede tocar» significa «tiene un métodoplay».class NotPlayableError extends Error.MusicShopcon un métododemoInstrument(instrument): si el instrumento puede tocar (typeof instrument.play === "function"), lo toca; si no, lanzaNotPlayableError.
- 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.
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.
4ingresa 50retira 500retira -5retira 120Saldo: 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).
María, 19 añosPablo: Edad no válida: -3Juan: Edad no válida: 200Más tareas
- Reserva de mesa. Una clase
Restaurantcon un número de mesas libres y un método para reservar una. Si se han acabado las mesas, lanza tu propiaNoTablesError. 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
Productcon una cantidad en existencias y un métodosell(qty). Si no hay suficiente en existencias, lanzaOutOfStockError, 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
NotPlayableErrorun campo con el instrumento que no puede tocar, e imprime su precio encatch.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces sin interface · Comprobaciones de tipo · Excepciones · try, catch y finally
- Continúa en: Datos en archivos · Datos, switch y desestructuración · Patrones de diseño · Tests con node:test
- Ejercicios sobre el tema: Errores y excepciones