Saltar al contenido
K
Swift: el lenguajePaso a paso38 min de lectura · 8 min de máquina Intermedio

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 un enum con 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 con throw y el control sale de ella al instante.
  • try: se escribe delante de cada llamada a una función throws, para dejar a la vista que ahí puede pasar algo. Quien llama decide qué hacer con el fallo (do/catch para reaccionar, try? para convertirlo en nil…).

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:

ejemplo mínimo: throws, try, do-catch, try? y Result (con comentarios)
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:

EscribesQué pasa si fallaCuándo
try dentro de do { } catch { }Salta al catch, con el error en errorQuieres reaccionar al motivo
try?El resultado es nil y el error se descartaSolo importa si salió o no
try!El programa se detieneFallar sería un fallo tuyo, no de los datos
Result { try … }El error queda guardado en .failureQuieres 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:

ejemplo mínimo: catch con patrón (con comentarios)
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: sinNombre

Un 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. LocalizedError viene de ahí.
  • errorDescription devuelve String?: nil significaría «sin texto propio».
  • (1...8).contains(cuantos): un rango cerrado (del 1 al 8, los dos incluidos) y la pregunta «¿está dentro?».
ejemplo mínimo: LocalizedError, un rango y guard que lanza (con comentarios)
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 mesa

Error 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.

con el bug: try! con un texto que escribe el usuario
let textoDelUsuario = "diez"
let dorsal = try! dorsalValido(textoDelUsuario)   // compila sin aviso
print(dorsal)
corregido: el error se atiende
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

KotlinSwift
Cualquier función puede lanzar; no se declarathrows 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 compilaLlamar 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?: nil
Ver solución
solución: la hucha
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.

Torneo
Sources
Torneo
Errores.swiftse crea
Lo que tocas ahora, en el Project navigator de Xcode con el paquete Torneo abierto.

Los ficheros viven en ~/Marcador-Swift/Torneo.

Crea el fichero.

  1. En el Project navigator (el panel de la izquierda de Xcode; se muestra u oculta con Cmd+1), despliega Sources, clic derecho sobre la carpeta Torneo (la de dentro de Sources) → New File from Template… → Swift File → Next, escribe Errores y pulsa Create. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal, en ~/Marcador-Swift/Torneo, con touch Sources/Torneo/Errores.swift.
  2. 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):

CasoTexto
equipoContraSiMismoUn equipo no puede jugar contra sí mismo
equiposNoInscritosLos dos equipos deben estar inscritos en Copa (con el nombre del torneo)
minutoFueraDelPartidoMinuto fuera del partido: 200 (con el minuto)
golDeEquipoAjenoEl gol es de un equipo que no juega: Lobos
jugadorNoJuegaEnElEquipoLuis no juega en Rayo (jugador y equipo)
jugadorNoJuegaElPartidoPedro no juega este partido
cambioConAjenosEl cambio implica a alguien que no juega este partido
  • ErrorDeTorneo: un error público que se pueda comparar con == (adopta Equatable) y tiene los siete casos de la tabla. Cada caso da su texto legible, el de la tabla. Llevan datos, con estas etiquetas:
    • equiposNoInscritos: el torneo.
    • minutoFueraDelPartido: el minuto, sin etiqueta.
    • golDeEquipoAjeno: el equipo.
    • jugadorNoJuegaEnElEquipo: el jugador y el equipo.
    • jugadorNoJuegaElPartido: el jugador.
    • 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, y fueraDeRango, 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 (usa dorsalDesdeTexto, de guard let y textos que no son números), lanza el noEsUnNumero de ErrorDeDorsal con ese texto; si no está entre 1 y 99, el fueraDeRango con 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
Torneo/Sources/Torneo/Errores.swift
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"
        }
    }
}
Torneo/Sources/Torneo/Errores.swift
// 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.

Torneo
Sources
torneo-cli
main.swiftse modifica
Lo que tocas en este paso: un fichero que cambia.

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?: nil
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/torneo-cli/main.swift
// `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-cli

El 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-cli termina con «10» -> dorsal 10, «diez» no es un número, «150» -> fueraDeRango(150) y Con try?: nil.
  • xcrun swift test termina con Test 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! y Result, 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. 1.¿Qué obliga el compilador a hacer cuando llamas a una función marcada con throws?

  2. 2.¿Qué vale let d = try? dorsalValido("diez") si dorsalValido lanza un error con ese texto?

  3. 3.¿Cuándo es razonable usar try!?

  4. 4.¿Qué escribes en Swift para lo que en Kotlin es require(cond) { "mensaje" }?

  5. 5.¿Para qué sirve errorDescription al adoptar LocalizedError?

Faltan 5 de 5