Swift de Cero a Experto #12: Manejo de Errores
throw, try, do/catch, defer, rethrows, Result y los typed throws de Swift 6. Un error no es una excepción mágica — es solo un valor de un tipo que conforma Error, enrutado por tu call stack tan barato como un return.
En el artículo anterior trazamos una línea: un opcional es la forma de Swift de decir “aquí podría no haber valor”. Pero la ausencia no siempre cuenta toda la historia. Cuando Int("hello") retorna nil, sabes que falló — pero no sabes por qué. Para convertir un string a entero, “no se parseó” es suficiente. Para “abre este archivo”, no lo es: ¿el archivo no existía? ¿Estaba bloqueado? ¿Corrupto? El manejo de errores es la mitad que los opcionales dejan fuera — un fallo esperado, con una razón adjunta.
Y así como un opcional resultó ser solo un enum, un error resulta ser solo un valor. No hay objeto de excepción especial, ni magia en runtime desenrollando el stack. Un error de Swift es un valor de un tipo que conforma un único protocolo vacío — y throw, try y catch son la fontanería que enruta ese valor desde donde ocurrió hasta donde lo manejas.
Los errores de Swift no son excepciones. Lanzar un error tiene las características de rendimiento de un return, no de una catástrofe que desenrolla el stack. Un error es un valor que viaja hacia arriba por la cadena de llamadas — nada más exótico que eso.
El protocolo Error: los errores son valores
Como Error no tiene requisitos, cualquier tipo puede ser un error. Pero los enumerations del #6 encajan de forma natural: un grupo de condiciones de fallo relacionadas es exactamente un conjunto de cases, y los valores asociados dejan que cada case lleve detalle extra sobre qué salió mal.
enum VendingMachineError: Error { case invalidSelection case insufficientFunds(coinsNeeded: Int) case outOfStock}insufficientFunds no solo dice “no hay suficiente dinero” — lleva cuántas monedas más hacen falta. Ese es el punto entero de un error frente a un mero nil: responde por qué, no solo si.
Para señalar un fallo, lanzas (throw) uno de estos valores:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)Una sentencia throw transfiere el control de inmediato, exactamente como return — solo que en vez de entregar un valor de vuelta al que llama, entrega un valor de error hacia arriba por la cadena hasta que alguien lo atrapa.
throws: marcar una función que puede fallar
func canThrowErrors() throws -> String
func cannotThrowErrors() -> StringLa palabra clave throws es un contrato, exigido por el compilador en ambas direcciones. Dentro de la función, tienes permiso de hacer throw. Fuera, todo llamador está obligado a reconocer que podría lanzar.
Este es el ejemplo canónico del libro de Swift — una máquina expendedora cuyo método vend usa el guard de salida temprana del #5 para lanzar el error correcto en el instante en que falla una precondición:
struct Item { var price: Int var count: Int}
class VendingMachine { var inventory = [ "Candy Bar": Item(price: 12, count: 7), "Chips": Item(price: 10, count: 4), "Pretzels": Item(price: 7, count: 11), ] var coinsDeposited = 0
func vend(itemNamed name: String) throws { guard let item = inventory[name] else { throw VendingMachineError.invalidSelection } guard item.count > 0 else { throw VendingMachineError.outOfStock } guard item.price <= coinsDeposited else { throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited) }
coinsDeposited -= item.price var newItem = item newItem.count -= 1 inventory[name] = newItem print("Dispensing \(name)") }}Como un throw transfiere el control de inmediato, el artículo se entrega solo si cada guard pasa. El happy path queda plano y sin indentar; los fallos se desprenden arriba.
try: el costo visible de llamar a algo que lanza
No puedes llamar a una throwing function en silencio. Cada llamada a una debe marcarse con try — una palabra clave que hace visible la posibilidad de fallo en el punto de llamada.
let favoriteSnacks = [ "Alice": "Chips", "Bob": "Licorice", "Eve": "Pretzels",]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws { let snackName = favoriteSnacks[person] ?? "Candy Bar" try vendingMachine.vend(itemNamed: snackName)}buyFavoriteSnack no maneja el error — no tiene do/catch. Así que debe ser ella misma throws, y el error de vend fluye directo a través de ella hasta su llamador. Eso es propagación: un error sube por cada try de su camino hasta que algún scope decide detenerlo.
Los initializers que lanzan propagan igual — útil cuando el contrato de init del #10 no puede satisfacerse porque una dependencia falló:
struct PurchasedSnack { let name: String init(name: String, vendingMachine: VendingMachine) throws { try vendingMachine.vend(itemNamed: name) self.name = name }}
Cada try es una admisión: “la llamada que estoy por hacer podría no terminar normalmente”. Swift se niega a ocultártelo — la propagación siempre se escribe, un try a la vez.
do / catch: detener el error
Las cuatro formas de manejar un error lanzado son: propagarlo (acabamos de verlo), convertirlo en opcional, afirmar que no puede ocurrir o — el caballo de batalla — atraparlo con do/catch.
La forma general te deja comparar errores específicos, agregar condiciones where, agrupar patrones con comas y terminar con un catch-all:
var vendingMachine = VendingMachine()vendingMachine.coinsDeposited = 8do { try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine) print("Success! Yum.")} catch VendingMachineError.invalidSelection { print("Invalid Selection.")} catch VendingMachineError.outOfStock { print("Out of Stock.")} catch VendingMachineError.insufficientFunds(let coinsNeeded) { print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")} catch { print("Unexpected error: \(error).")}// Insufficient funds. Please insert an additional 2 coins.Fíjate en catch VendingMachineError.insufficientFunds(let coinsNeeded) — esto es de nuevo el pattern matching de enums del #6, ligando el valor asociado directo desde el error atrapado. Y el catch pelado final es especial:
Manejo parcial y re-propagación
Las cláusulas catch no tienen que manejar todos los errores posibles. Si ninguna coincide, el error sigue propagándose al scope que rodea — que entonces carga con la misma responsabilidad. Puedes atrapar por tipo con el patrón is, manejando una familia entera de una vez y dejando pasar todo lo demás:
func nourish(with item: String) throws { do { try vendingMachine.vend(itemNamed: item) } catch is VendingMachineError { print("Couldn't buy that from the vending machine.") }}
do { try nourish(with: "Beet-Flavored Chips")} catch { print("Unexpected non-vending-machine-related error: \(error)")}// Couldn't buy that from the vending machine.O lista varios errores relacionados tras un solo catch, separados por comas:
func eat(item: String) throws { do { try vendingMachine.vend(itemNamed: item) } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock { print("Invalid selection, out of stock, or not enough money.") }}try? y try!: colapsar un error en un opcional (o en un crash)
A veces no quieres la ceremonia de un do/catch. Dos variantes de try encogen todo el asunto a una sola palabra clave — y este es exactamente el puente de vuelta a los opcionales del capítulo pasado que prometimos.
func someThrowingFunction() throws -> Int { // ...}
let x = try? someThrowingFunction()
// x se comporta exactamente como y aquí abajo:let y: Int?do { y = try someThrowingFunction()} catch { y = nil}Esa es la personalidad entera de try?: tira el por qué y se queda solo con el si, convirtiendo un error de vuelta en el nil con el que empezamos el capítulo pasado. Brilla cuando quieres intentar varios enfoques y caer al siguiente, apoyándote en el if-let binding del #11:
func fetchData() -> Data? { if let data = try? fetchDataFromDisk() { return data } if let data = try? fetchDataFromServer() { return data } return nil}El hermano temerario es try!:
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
defer: limpieza que siempre corre
Abres un archivo, debes cerrarlo. Adquieres un lock, debes liberarlo. El problema: entre adquirir y liberar, cualquier cosa podría hacer throw — y una salida temprana saltaría tu limpieza. defer resuelve esto.
func processFile(filename: String) throws { if exists(filename) { let file = open(filename) defer { close(file) } while let line = try file.readline() { // Work with the file. } // close(file) corre aquí, al final del scope — // y con la misma fiabilidad si file.readline() lanza. }}El try file.readline() podría lanzar a media iteración. Sin defer, el close(file) tras el loop se saltaría en ese camino. Con él, close(file) está garantizado de correr a la salida — haya error o no.

rethrows: lanzar solo si tu closure lanza
Considera una función que recibe un closure y lo llama — como map. ¿Debería ser throws? Solo si el closure que le pasas lanza. Si pasas un closure que no lanza, la función no puede fallar, y forzar un try en cada llamada sería ruido. rethrows expresa exactamente este contrato condicional.
func transform(_ value: Int, using f: (Int) throws -> Int) rethrows -> Int { try f(value)}
// closure que no lanza -> no hace falta try:let doubled = transform(21) { $0 * 2 }
// closure que lanza -> se requiere try:let parsed = try transform(21) { try riskyTransform($0) }Por eso las funciones de orden superior de la librería estándar sobre colecciones — map, filter, reduce, las que conocimos con los closures del #7 — son rethrows. Se mantienen invisibles cuando tu closure es puro, y se vuelven throwing en el instante en que les entregas trabajo que puede fallar.
Result: un error que puedes guardar y pasar
throw/try/catch enrutan un error a través del flujo de control — ocurre, se propaga, lo atrapas, se fue. Pero a veces necesitas guardar el resultado de una operación que puede fallar como un valor común: para entregarlo a un callback de completado, almacenarlo, compararlo o transformarlo después. Para eso es Result.
Reducido a lo esencial, es un enum que casi podrías haber escrito tú:
enum Result<Success, Failure> where Failure: Error { case success(Success) case failure(Failure)}Lo consumes con el mismo pattern matching de switch del #6:
func loadConfig() -> Result<Config, ConfigError> { /* ... */ }
switch loadConfig() {case .success(let config): print("Loaded \(config).")case .failure(let error): print("Failed: \(error).")}El puente entre los dos mundos — errores lanzados y resultados guardados — viene incorporado en el tipo. El initializer init(catching:) corre un closure que lanza y captura el resultado en un Result; get() hace lo inverso, re-lanzando el error guardado si lo hay:
// mundo del throw -> mundo del valor:let result = Result { try someThrowingFunction() }// result es Result<Int, any Error>
// mundo del valor -> mundo del throw:do { let value = try result.get() // retorna el success, o re-lanza el failure print(value)} catch { print("Got error: \(error)")}Aquí any Error significa “algún valor cuyo tipo concreto conforma a Error, decidido en tiempo de ejecución” — la palabra clave any indica borrado de tipos (type erasure); lo veremos como corresponde con los protocolos en el #13.
Typed throws: nombrar el error
Todo lo anterior borra el tipo del error (type-erases). Una función con throws simple puede lanzar cualquier Error — en un catch, todo lo que sabes estáticamente es “algún error”, razón por la cual recurres a un catch-all. La mayor parte del tiempo ese es el default correcto: las versiones nuevas de una librería lanzan errores nuevos, y no quieres conocerlos todos de antemano.
Pero ocasionalmente sí puedes enumerar cada fallo — una librería pequeña, un sistema embebido, un closure que solo reenvía un error conocido — y te gustaría que el compilador te lo exigiera. Los typed throws (Swift 6) te dejan nombrar el tipo de error exacto.
enum StatisticsError: Error { case noRatings case invalidRating(Int)}
func summarize(_ ratings: [Int]) throws(StatisticsError) { guard !ratings.isEmpty else { throw .noRatings }
var counts = [1: 0, 2: 0, 3: 0] for rating in ratings { guard rating > 0 && rating <= 3 else { throw .invalidRating(rating) } counts[rating]! += 1 } print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)}Dos recompensas ya son visibles. Primera, como el tipo de error es fijo, puedes escribir la forma corta throw .noRatings en vez de throw StatisticsError.noRatings — Swift infiere el tipo. Segunda, si intentaras hacer throw VendingMachineError.outOfStock dentro de summarize, fallaría en tiempo de compilación: ese no es el tipo de error declarado.
Una función con typed throws encaja limpiamente en el mundo sin tipar — StatisticsError es un any Error válido:
func someThrowingFunction() throws { // es decir, throws(any Error) let ratings = [1, 2, 3, 2, 2, 1] try summarize(ratings)}También puedes tipar un do-catch. La recompensa es un catch exhaustivo y type-safe — el error atrapado es un StatisticsError concreto, así que puedes hacer switch sobre él sin catch-all:
let ratings: [Int] = []do throws(StatisticsError) { try summarize(ratings)} catch { switch error { // error es un StatisticsError, no any Error case .noRatings: print("No ratings available") case .invalidRating(let rating): print("Invalid rating: \(rating)") }}// No ratings availablethrows a secas dice “algo podría salir mal — no prometo qué”. throws(E) dice “solo un E puede salir mal, y el compilador me lo exigirá”. throws(Never) dice “nada puede”. Tres puntos precisos en un mismo dial.
Recapitulación

- Protocolo Error — los errores son valores de tipos que conforman el protocolo vacío
Error; los enums con valores asociados encajan de forma natural - throw — transfiere el control de inmediato, como
return, pero entrega un error hacia arriba por la cadena; tan barato como un return, no una excepción - throws — marca una función que puede fallar; solo las throwing functions propagan, y los llamadores deben reconocerlo con
try - try / try? / try! —
trypropaga;try?convierte el error ennil(unT?);try!afirma que no hay error y hace trap si se equivoca - do / catch — detiene un error lanzado; el
catchpelado ligaerror; las cláusulas pueden comparar por patrón, por tipo conis, o por listas separadas por comas; un error sin manejar a nivel superior crashea - defer — limpieza que corre en cada camino de salida; varios defers corren en orden inverso
- rethrows — lanza solo si un argumento closure lanza; cómo
map/filter/reducese mantienen sin lanzar para closures puros - Result<Success, Failure> — manejo de errores como valor guardado;
Result { try f() }captura,get()re-lanza - Typed throws —
throws(E)nombra el tipo de error exacto para exhaustividad en tiempo de compilación;throwsa secas esthrows(any Error);throws(Never)no puede lanzar
Lo que viene
En el próximo artículo exploramos los Protocols — los contratos que dejan a tipos no relacionados compartir una interfaz sin compartir una superclase. Ya nos apoyamos en uno: Error es un protocolo, y conformarlo es lo que hizo lanzables a nuestros enums. A continuación veremos cómo los protocolos definen requisitos, cómo los tipos los adoptan, y por qué el diseño orientado a protocolos es la columna vertebral del Swift idiomático.
Nos vemos la próxima semana.
Un opcional respondía “¿hay un valor?”. Un error responde “y si no, ¿por qué no?”. Juntos cubren los dos modos de fallo honestos de cualquier operación — y ninguno es magia. Uno es un enum con dos cases; el otro es un valor enrutado hacia arriba por tu call stack. Una vez que lo ves, el manejo de errores deja de dar miedo y empieza a ser solo otra forma que tus datos pueden tomar.
Referencias
Relacionados
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #15: Tipos opacos y tipos de protocolo encajonados
La tercera puerta para salir de 'no quiero nombrar el tipo concreto'. some oculta un tipo concreto conservando su identidad — sin caja, en tiempo de compilación. any borra el tipo en una caja de runtime. Así devuelves por fin un protocolo que tiene un associatedtype.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #14: Genéricos
Un solo swapTwoValues, un solo Stack<Element>, un solo findIndex — escritos una vez, funcionando para todo tipo. Los genéricos dejan que el llamador elija el tipo concreto, que el compilador especialice el código, y que la caja del #13 desaparezca. Array, Dictionary, Optional y Result fueron genéricos todo el tiempo.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #13: Protocolos
Un protocolo es un contrato, no una clase. Dice qué debe hacer un tipo sin decir qué es — y esa sola idea impulsa la delegación, el Equatable sintetizado, los existenciales y las extensiones de protocolo que hacen idiomático a Swift.