Java throw y throws
Lanza excepciones en Java con throw y decláralas en firmas de métodos con throws. Aprende cuándo y cómo usarlas correctamente.
Hasta ahora hemos estado capturando excepciones lanzadas por otro código. Ahora verás cómo lanzar las tuyas propias. Dos palabras clave hacen la mayor parte del trabajo — y es fácil confundirlas porque solo se diferencian en una letra.
throw— una sentencia que lanza una excepción en tiempo de ejecución. Una palabra, en código que se ejecuta.throws— una declaración en la firma de un método que dice "este método puede lanzar estos tipos de excepción." El compilador la verifica, nunca se ejecuta.
throw ocurre. throws advierte. Ten ese par en mente.
Lanzar una excepción
throw toma una expresión de tipo Throwable (o cualquier subtipo) y la lanza. El método actual sale de inmediato, la pila comienza a desenrollarse y la excepción inicia su búsqueda de un catch coincidente.
if (amount < 0) {
throw new IllegalArgumentException("amount must be non-negative, got " + amount);
}Tres detalles:
- Solo puedes lanzar un
Throwable. El compilador lo exige —throw "oops";no compilará. - Siempre lanzas una instancia, no una clase.
throw new X(...), nuncathrow X. - La instancia puede ser una que construiste en línea (lo más común), o un objeto preexistente (raro — las excepciones llevan trazas de pila desde su construcción, así que reutilizar una congela la traza incorrecta).
Cuándo lanzar
Lanza una excepción cuando el método actual no puede cumplir su contrato. Algunos casos claros:
- Argumentos inválidos —
IllegalArgumentExceptionpara "me llamaste de forma incorrecta." - Estado incorrecto —
IllegalStateExceptionpara "me llamaste en el momento equivocado" (p. ej.next()en un iterador vacío). - Datos faltantes — excepciones específicas del dominio como
UserNotFoundException. - Operaciones externas fallidas — errores de IO, errores de red. Normalmente estos provienen de la llamada que acabas de hacer, por lo que no los construyes tú mismo; los dejas propagarse o los envuelves en una excepción de nivel superior.
El caso en que no debes lanzar: como atajo de flujo de control para resultados normales. "Lanzar para controlar el flujo" es lento y confuso. Si "no encontrado" es un resultado rutinario, devuelve un Optional<T>, no una NotFoundException.
Elegir un tipo
Las excepciones integradas de java.lang cubren la mayoría de los casos sin ceremonias:
IllegalArgumentException— argumento incorrectoIllegalStateException— estado incorrectoNullPointerException— un argumento requerido era null (usaObjects.requireNonNull)UnsupportedOperationException— operación no implementada (p. ej.addde una lista inmutable)ArithmeticException— error matemático
Cuando el fallo es específico de tu dominio — "usuario no encontrado," "cupón inválido," "configuración desincronizada" — escribe una pequeña clase personalizada para ello. En dos capítulos haremos exactamente eso.
La cláusula throws
Si tu método puede lanzar una excepción verificada que no captura él mismo, debes declararla:
public Config loadConfig(Path p) throws IOException, ParseException {
String text = Files.readString(p);
return parser.parse(text);
}La cláusula es parte del contrato del método. Le dice a cada invocador: "si me llamas, tienes que capturar estas o declararlas tú mismo." El compilador lo exige — eso es lo que las hace verificadas.
Algunas reglas:
- Solo declaras excepciones verificadas. Los
RuntimeExceptiony sus subclases son no verificadas — declararlas está permitido pero no es obligatorio, y normalmente no se hace. - Puedes declarar más tipos de los que realmente lanzas — útil cuando quieres dejar abierta la opción para implementaciones futuras, aunque es un ruido leve.
- Un método que sobreescribe otro puede declarar las mismas o menos excepciones verificadas que el padre (y solo subtipos de las declaradas). No puede agregar nuevas. Esto es la sustitución de Liskov aplicada a las excepciones.
throw y throws juntos
Un método real normalmente hace ambas cosas:
public User loadUser(String id) throws IOException {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("id must be non-blank");
}
String json = httpClient.get("/users/" + id); // may throw IOException
return parser.toUser(json);
}- El
throws IOExceptiondeclara la excepción verificada que puede provenir dehttpClient.get. - El
throw new IllegalArgumentException(...)lanza una no verificada para entrada incorrecta. No necesita aparecer en la cláusulathrows.
Envolver una excepción
Cuando una excepción de bajo nivel no tiene significado en tu capa, envuélvela en una que sí lo tenga. Pasa el original como causa para que la traza permanezca intacta:
try {
return Files.readString(configPath);
} catch (IOException e) {
throw new ConfigLoadException("could not load config from " + configPath, e);
}El patrón del constructor de segundo argumento — (message, cause) — es estándar en Exception, IOException y todos los integrados. Cuando escribas tu propia clase de excepción, dale ambos constructores.
Un ejemplo trabajado
Un pequeño ayudante de estilo bancario que valida la entrada con IllegalArgumentException, señala una cuenta vacía con IllegalStateException, y deja que una excepción verificada ascienda al invocador mediante throws. El controlador muestra cómo se ve cada una cuando se lanza.
Los tres casos en tiempo de ejecución — argumento, estado y retiro exitoso — caen a través de un catch. El cuarto, archive(), solo compila porque main puede capturar Exception y porque archive() declaró throws ArchiveException. Intenta eliminar la cláusula throws y el programa no compilará.
Qué sigue
El compilador trata algunas excepciones estrictamente (debes manejarlas) y otras con permisividad (no tienes que hacerlo). Esa división es el próximo capítulo. Continúa en Excepciones verificadas vs. no verificadas en Java.