Errores: throws, try y do/catch
Al terminar: Lanzar y capturar errores con throws, do/try/catch, try? y Result.
Hasta ahora, cuando algo salía mal en tu código, solo había dos salidas: devolver nil (que no dice por qué falló) o que el programa se detuviera. Al terminar esta lección tendrás una tercera: una función que lanza un error con el motivo, y un main que lo atiende y sigue. Lo estrenas con el dorsal de un jugador: el texto «10» da el dorsal 10, «diez» dice que no es un número y «150» dice que se sale del rango.
Temas: throws, try, do/catch, try?, try! y Result; errores propios con enum y LocalizedError. En la lección siguiente, registrar usará estos mismos errores.
Lo que necesitas saber antes de empezar
Tres palabras de Swift para entender de qué va la lección; se explican a fondo en cuanto el código las usa:
Error: un protocolo (la lista de cosas que sabe hacer un tipo) que dice «este tipo describe algo que ha salido mal». Un error propio suele ser unenumcon un caso por cada fallo posible.throws(lanza): se escribe en la firma de una función para avisar de que puede fallar. Esa función no devuelve el error: lo lanza conthrowy el control sale de ella al instante.try: se escribe delante de cada llamada a una funciónthrows, para dejar a la vista que ahí puede pasar algo. Quien llama decide qué hacer con el fallo (do/catchpara reaccionar,try?para convertirlo ennil…).
throws: la función avisa de que puede fallar
En Swift no hay excepciones «sorpresa». Una función que puede fallar lo escribe en su firma con throws, y cada llamada lleva delante try. El compilador no te deja olvidarlo.
Un ejemplo mínimo, en tu Playground:
enum ErrorDeTiempo: Error { case negativo(Int) }
// `throws` va después de los parámetros: esta función puede lanzar un error.
func minutosRestantes(_ jugados: Int) throws -> Int {
// `guard condición else { … }`: si la condición falla, hay que salir de la
// función (aquí, lanzando el error). Es el `require` de Kotlin.
guard jugados >= 0 else { throw ErrorDeTiempo.negativo(jugados) }
return 90 - jugados
}
print(try minutosRestantes(30)) // 60
// `do/catch`: si lo de dentro lanza, salta al `catch` con el error en `error`.
// `_ =` descarta el valor que devuelve la función: aquí solo importa el error.
do { _ = try minutosRestantes(-5) } catch { print("error:", error) }
// `try?`: si falla, el resultado es nil y el error se descarta.
// `String(describing:)` convierte un valor en texto; con un opcional evita el
// aviso que da `print` al recibir un `Int?` directamente (es lo mismo que
// escribir `print(… as Any)`, que verás en otras lecciones).
print(String(describing: try? minutosRestantes(-5))) // nil
// `Result { … }`: guarda el valor o el error, como un valor más.
let resultado = Result { try minutosRestantes(-5) }
print(resultado)Verás esto (en la última línea, el error puede llevar delante el nombre del módulo donde corre el código, que depende de dónde lo ejecutes; en tu Playground no está garantizado que salga igual, y no importa):
60
error: negativo(-5)
nil
failure(ErrorDeTiempo.negativo(-5))En el código de nivel superior (un Playground, o el main.swift del paquete) puedes escribir try sin do/catch: si el error no se atiende, el programa termina. Dentro de una función hace falta do/catch o declarar throws.
Las cuatro maneras de atender una llamada que lanza:
| Escribes | Qué pasa si falla | Cuándo |
|---|---|---|
try dentro de do { } catch { } | Salta al catch, con el error en error | Quieres reaccionar al motivo |
try? | El resultado es nil y el error se descarta | Solo importa si salió o no |
try! | El programa se detiene | Fallar sería un fallo tuyo, no de los datos |
Result { try … } | El error queda guardado en .failure | Quieres pasar el resultado a otro sitio |
Un catch que filtra un caso
Un catch puede filtrar por caso: catch ErrorDeReserva.mesaOcupada(let mesa) { … } atiende solo ese error (y se queda con el dato que lleva, aquí la mesa), y el catch { } final atiende todos los demás. Se ve en un restaurante que acepta reservas, donde la mesa 3 siempre está ocupada:
enum ErrorDeReserva: Error {
case sinNombre // falta el nombre
case mesaOcupada(Int) // con el dato que falló: qué mesa
}
func reservar(nombre: String, mesa: Int) throws -> String {
guard !nombre.isEmpty else { throw ErrorDeReserva.sinNombre }
guard mesa != 3 else { throw ErrorDeReserva.mesaOcupada(mesa) }
return "\(nombre), mesa \(mesa)"
}
// Primero una reserva en la mesa 3 y luego otra en la 2, las dos con nombre.
for mesa in [3, 2] {
do {
print(try reservar(nombre: "Lucía", mesa: mesa))
} catch ErrorDeReserva.mesaOcupada(let ocupada) { // solo este caso
print("La mesa \(ocupada) está ocupada")
} catch { // todos los demás
print("Otro error: \(error)")
}
}
// Sin nombre falla por otro motivo: lo atiende el `catch` final.
do {
print(try reservar(nombre: "", mesa: 1))
} catch ErrorDeReserva.mesaOcupada(let ocupada) {
print("La mesa \(ocupada) está ocupada")
} catch {
print("Otro error: \(error)")
}La mesa 3 está ocupada
Lucía, mesa 2
Otro error: sinNombreUn texto legible para cada error: LocalizedError
Un error se imprime con el nombre de su caso (mesaOcupada(3)), que sirve al programador pero no a quien lee la pantalla. El protocolo LocalizedError añade una propiedad, errorDescription, con el texto legible de cada caso; error.localizedDescription la lee. Tres piezas nuevas en el ejemplo:
import Foundation: trae la librería base de Apple.LocalizedErrorviene de ahí.errorDescriptiondevuelveString?:nilsignificaría «sin texto propio».(1...8).contains(cuantos): un rango cerrado (del 1 al 8, los dos incluidos) y la pregunta «¿está dentro?».
import Foundation // trae LocalizedError
enum ErrorDeReserva: Error {
case sinNombre
case demasiadosComensales(Int) // con el dato que falló
}
// LocalizedError añade errorDescription: el texto legible de cada caso.
extension ErrorDeReserva: LocalizedError {
var errorDescription: String? { // String?: nil sería «sin texto propio»
switch self {
case .sinNombre:
"La reserva necesita un nombre"
case let .demasiadosComensales(cuantos):
"No caben \(cuantos) personas en una mesa"
}
}
}
func comensalesValidos(_ cuantos: Int) throws -> Int {
// (1...8).contains: ¿está entre 1 y 8, los dos incluidos?
guard (1...8).contains(cuantos) else {
throw ErrorDeReserva.demasiadosComensales(cuantos) // sale de la función aquí
}
return cuantos // solo se llega si el guard pasó
}
do {
_ = try comensalesValidos(12)
} catch {
print(error.localizedDescription)
}No caben 12 personas en una mesaError típico: try! sobre datos de fuera
dorsalValido la escribes en el paso de abajo: recibe un texto y devuelve el dorsal, o lanza un ErrorDeDorsal. Por ahora basta con leer cómo se llama.
let textoDelUsuario = "diez"
let dorsal = try! dorsalValido(textoDelUsuario) // compila sin aviso
print(dorsal)let textoDelUsuario = "diez"
do {
let dorsal = try dorsalValido(textoDelUsuario)
print(dorsal)
} catch {
print("Dorsal no válido: \(error)")
}try! promete que el error no ocurrirá; si ocurre, el programa termina. Compila sin aviso y falla al ejecutar. En la consola del paquete, la última línea contiene Fatal error: 'try!' expression unexpectedly raised an error y sigue con el error. Verás algo como esto (el fichero y la línea serán los tuyos; el nombre del objetivo torneo-cli sale con guion bajo, torneo_cli, porque el nombre de un módulo es un identificador de Swift y no admite guiones):
torneo_cli/main.swift:155: Fatal error: 'try!' expression unexpectedly raised an error: Torneo.ErrorDeDorsal.noEsUnNumero("diez")Es el ! de los opcionales con otro nombre. Sirve para valores que tú controlas (un texto fijo que sabes válido), nunca para lo que llega de fuera.
Kotlin y Swift, lado a lado
| Kotlin | Swift |
|---|---|
| Cualquier función puede lanzar; no se declara | throws en la firma y try en cada llamada |
require(cond) { "mensaje" } | guard cond else { throw .error } |
try { } catch (e: X) { } | do { try … } catch let e as X { } (as X se queda con los errores que sean de ese tipo) |
runCatching { }.getOrNull() | try? |
getOrThrow() | try! (detiene el programa) o try propagando |
Ignorar un Result compila | Llamar a una función throws sin try no compila |
Reto · Ejercicio: sacar dinero de una hucha
En tu Playground, escribe un error con dos casos: la cantidad pedida no es positiva, o falta saldo (y este caso dice cuánto falta). Escribe una función que, dados una cantidad y un saldo, devuelva lo que queda, o lance el error que toque. Intenta sacar 20, 70 y 0 de una hucha con 50: si falta saldo, di cuánto falta; con cualquier otro error, imprímelo tal cual. Por último, convierte en nada el intento de sacar 70 e imprime el resultado. Lo que debe salir:
Sacas 20: quedan 30
Sacas 70: faltan 20
Sacas 0: cantidadNoPositiva
Con try?: nilVer solución
enum ErrorDeHucha: Error {
case cantidadNoPositiva
case saldoInsuficiente(faltan: Int)
}
func sacar(_ cantidad: Int, de saldo: Int) throws -> Int {
guard cantidad > 0 else { throw ErrorDeHucha.cantidadNoPositiva }
guard cantidad <= saldo else { throw ErrorDeHucha.saldoInsuficiente(faltan: cantidad - saldo) }
return saldo - cantidad
}
for cantidad in [20, 70, 0] {
do {
let quedan = try sacar(cantidad, de: 50)
print("Sacas \(cantidad): quedan \(quedan)")
} catch ErrorDeHucha.saldoInsuficiente(let faltan) {
print("Sacas \(cantidad): faltan \(faltan)")
} catch {
print("Sacas \(cantidad): \(error)")
}
}
print("Con try?: \(String(describing: try? sacar(70, de: 50)))")El primer catch filtra el caso que lleva un dato (faltan) y se queda con él; el catch final atiende todo lo demás, y \(error) imprime el nombre del caso.
Paso 1 · Crea Errores.swift
Objetivo. Un fichero nuevo en tu paquete Torneo con los errores del torneo y del dorsal, y con dorsalValido. Compila, y todavía no lo usa nadie.
Los ficheros viven en ~/Marcador-Swift/Torneo.
Crea el fichero.
- En el Project navigator (el panel de la izquierda de Xcode; se muestra u oculta con Cmd+1), despliega
Sources, clic derecho sobre la carpetaTorneo(la de dentro deSources) → New File from Template… → Swift File → Next, escribeErroresy pulsa Create. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal, en~/Marcador-Swift/Torneo, contouch Sources/Torneo/Errores.swift. - Comprueba que el fichero quedó dentro de
Sources/Torneo: el objetivo al que pertenece un fichero lo decide la carpeta donde está.
Escribe su contenido. Las piezas son las del ejemplo de arriba: un enum que adopta Error, LocalizedError con su errorDescription, guard que lanza y un rango.
Intenta escribirlo tú. Los textos legibles de ErrorDeTorneo son estos (los usarán los tests de la lección siguiente):
| Caso | Texto |
|---|---|
equipoContraSiMismo | Un equipo no puede jugar contra sí mismo |
equiposNoInscritos | Los dos equipos deben estar inscritos en Copa (con el nombre del torneo) |
minutoFueraDelPartido | Minuto fuera del partido: 200 (con el minuto) |
golDeEquipoAjeno | El gol es de un equipo que no juega: Lobos |
jugadorNoJuegaEnElEquipo | Luis no juega en Rayo (jugador y equipo) |
jugadorNoJuegaElPartido | Pedro no juega este partido |
cambioConAjenos | El cambio implica a alguien que no juega este partido |
ErrorDeTorneo: un error público que se pueda comparar con==(adoptaEquatable) y tiene los siete casos de la tabla. Cada caso da su texto legible, el de la tabla. Llevan datos, con estas etiquetas:equiposNoInscritos: eltorneo.minutoFueraDelPartido: el minuto, sin etiqueta.golDeEquipoAjeno: elequipo.jugadorNoJuegaEnElEquipo: eljugadory elequipo.jugadorNoJuegaElPartido: eljugador.- Los nombres van como texto.
ErrorDeDorsal: un error público que se pueda comparar con==, con dos casos:noEsUnNumero, con el texto que no era un número, yfueraDeRango, con el número que se salió.dorsalValido: una función pública que recibe un texto sin etiqueta y devuelve un dorsal o lanza un error. Si el texto no es un número (usadorsalDesdeTexto, de guard let y textos que no son números), lanza elnoEsUnNumerodeErrorDeDorsalcon ese texto; si no está entre 1 y 99, elfueraDeRangocon ese número.
Equatable permite comparar los errores con ==: los tests de la lección siguiente lo necesitan. Y los tests exigen estos nombres tal cual.
Qué se escribe y dónde. Todo en Errores.swift, en este orden: ErrorDeTorneo y su extensión arriba, ErrorDeDorsal debajo y dorsalValido al final.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Foundation
// Un error propio es un tipo cualquiera que adopta el protocolo `Error`; lo
// habitual es un `enum` con un caso por cada cosa que puede salir mal, y con
// datos asociados para saber DÓNDE falló (en Kotlin, una clase que extiende
// `Exception`, o simplemente `require` con un mensaje).
public enum ErrorDeTorneo: Error, Equatable {
case equipoContraSiMismo
case equiposNoInscritos(torneo: String)
case minutoFueraDelPartido(Int)
case golDeEquipoAjeno(equipo: String)
case jugadorNoJuegaEnElEquipo(jugador: String, equipo: String)
case jugadorNoJuegaElPartido(jugador: String)
case cambioConAjenos
}
// `LocalizedError` da un texto legible en `errorDescription`. Los mensajes son
// los mismos que los de los `require` de la versión Kotlin.
extension ErrorDeTorneo: LocalizedError {
public var errorDescription: String? {
switch self {
case .equipoContraSiMismo:
"Un equipo no puede jugar contra sí mismo"
case let .equiposNoInscritos(torneo):
"Los dos equipos deben estar inscritos en \(torneo)"
case let .minutoFueraDelPartido(minuto):
"Minuto fuera del partido: \(minuto)"
case let .golDeEquipoAjeno(equipo):
"El gol es de un equipo que no juega: \(equipo)"
case let .jugadorNoJuegaEnElEquipo(jugador, equipo):
"\(jugador) no juega en \(equipo)"
case let .jugadorNoJuegaElPartido(jugador):
"\(jugador) no juega este partido"
case .cambioConAjenos:
"El cambio implica a alguien que no juega este partido"
}
}
}// Un error para leer texto de fuera: una función que puede fallar de varias
// maneras y por eso declara `throws`.
public enum ErrorDeDorsal: Error, Equatable {
case noEsUnNumero(String)
case fueraDeRango(Int)
}
// `throws`: esta función puede terminar lanzando un error en vez de devolver un
// valor. Quien la llame TIENE que decidir qué hacer: `try` dentro de `do/catch`,
// `try?` (nil si falla), `try!` (se detiene si falla) o volver a lanzarlo.
public func dorsalValido(_ texto: String) throws -> Int {
guard let numero = dorsalDesdeTexto(texto) else {
throw ErrorDeDorsal.noEsUnNumero(texto)
}
guard (1...99).contains(numero) else {
throw ErrorDeDorsal.fueraDeRango(numero)
}
return numero
}Compila. En la Terminal, cd ~/Marcador-Swift/Torneo y luego xcrun swift build. Debe terminar con Build complete!. En Xcode, el mismo resultado es Product → Build (Cmd+B).
Paso 2 · main atiende los errores del dorsal
Objetivo. Ver los errores en acción: un bucle que prueba tres textos y deja que cada error diga lo que pasó, sin tumbar el programa.
En main.swift (Sources → torneo-cli), al final del fichero, después de print("Partidos vistos desde …"), escribe esto. Intenta escribirlo tú:
- Para cada uno de los textos «10», «diez» y «150», imprime el dorsal, o «no es un número» si el error es ese caso, o el error tal cual en cualquier otro caso.
- Por último, imprime el dorsal de «diez» convertido en nada si falla.
Lo que debes ver al final de la salida:
«10» -> dorsal 10
«diez» no es un número
«150» -> fueraDeRango(150)
Con try?: nilIntenta primero. Cuando termines, compara con la solución.Mostrar el código
// `throws` de verdad: `try`, `try?` y `do/catch` con patrones.
for texto in ["10", "diez", "150"] {
do {
print("«\(texto)» -> dorsal \(try dorsalValido(texto))")
} catch ErrorDeDorsal.noEsUnNumero(let escrito) {
print("«\(escrito)» no es un número")
} catch {
print("«\(texto)» -> \(error)")
}
}
print("Con try?: \(String(describing: try? dorsalValido("diez")))")Compila y ejecuta, desde ~/Marcador-Swift/Torneo:
xcrun swift build
xcrun swift run torneo-cliEl error ErrorDeTorneo ya existe, pero nadie lo lanza todavía: en la lección siguiente, registrar lo estrena.
Punto de control
xcrun swift build(desde~/Marcador-Swift/Torneo) compila sin errores.xcrun swift run torneo-clitermina con«10» -> dorsal 10,«diez» no es un número,«150» -> fueraDeRango(150)yCon try?: nil.xcrun swift testtermina conTest run with 58 tests in 8 suites passed: los mismos de la lección anterior, porque hoy no hay tests nuevos.- Sabes decir cuándo usarías
try,try?,try!yResult, y por quétry!con texto de fuera es un error tuyo.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Qué obliga el compilador a hacer cuando llamas a una función marcada con
throws?2.¿Qué vale
let d = try? dorsalValido("diez")sidorsalValidolanza un error con ese texto?3.¿Cuándo es razonable usar
try!?4.¿Qué escribes en Swift para lo que en Kotlin es
require(cond) { "mensaje" }?5.¿Para qué sirve
errorDescriptional adoptarLocalizedError?