Guía del Lenguaje
Manejo de Errores

Manejo de errores

Cómo responder ante errores y recuperarse de estos.

El manejo de errores es el proceso de responder y recuperarse de situaciones de error en tu programa. Swift ofrece soporte de primera clase para arrojar, capturar, propagar, y manipular errores recuperables en tiempo de ejecución.

Algunas operaciones no siempre garantizan el total cumplimiento de la ejecución o la producción de una salida útil. Los opcionales se utilizan para representar la ausencia de un valor, pero cuando una operación falla, suele ser útil entender la causa de la falla, para que tu código pueda responder en consecuencia.

Como ejemplo, considera la tarea de leer y procesar datos desde un archivo en disco. Hay diversas maneras en las que esta tarea puede fallar, incluyendo que el archivo no exista en la ruta especificada, que el archivo no tenga permisos de lectura, o que el archivo no esté codificado en un formato compatible. Distinguir entre estas diferentes situaciones le permite a un programa resolver algunos errores y comunicarle al usuario sobre cualquier error que no pueda resolver.

Representación y lanzamiento de errores

En Swift, los errores se representan mediante valores de tipos que se ajustan al protocolo Error. Este protocolo vacío indica que un tipo puede ser utilizado para el manejo de errores.

Las enumeraciones de Swift son particularmente adecuadas para modelar un grupo de condiciones de error relacionadas, con valores asociados que permiten comunicar información adicional sobre la naturaleza de un error a comunicar. Por ejemplo, así es como podrías representar las condiciones de error asociadas con la operación de una máquina expendedora dentro de un juego:

enum ErrorMaquinaExpendedora: Error {
    case seleccionInvalida
    case fondosInsuficientes(monedasRequeridas: Int)
    case agotado
}

Arrojar un error te permite indicar que algo inesperado ha ocurrido y que el flujo normal de ejecución no puede continuar. Para arrojar un error, utiliza una sentencia throw. Por ejemplo, el siguiente código arroja un error para indicar que la máquina expendedora requiere cinco monedas adicionales:

throw ErrorMaquinaExpendedora.fondosInsuficientes(monedasRequeridas: 5)

Manejo de errores

Cuando se arroja un error, algún segmento de código circundante debe hacerse responsable de manejar el error; por ejemplo, corrigiendo el problema, intentando un enfoque alternativo, o informando al usuario sobre el fallo.

Hay cuatro maneras de manejar los errores en Swift. Puedes propagar el error desde una función al código que llama a dicha función, manejar el error usando una sentencia do-catch, manejar el error como un valor opcional, o afirmar que el error no ocurrirá. Cada enfoque se describe en una sección más adelante.

Cuando una función arroja un error, el flujo de tu programa cambia, por lo que es importante que puedas identificar rápidamente los lugares de tu código que puedan arrojar errores. Para identificar estos lugares en tu código, escribe la palabra clave try —o una de sus variantes: try? o try!— antes de una parte de código que invoque a una función, método o inicializador que pueda lanzar un error. Estas palabras clave se describen en las siguientes secciones.

Propagating Errors Using Throwing Functions

To indicate that a function, method, or initializer can throw an error, you write the throws keyword in the function's declaration after its parameters. A function marked with throws is called a throwing function. If the function specifies a return type, you write the throws keyword before the return arrow (->).

func canThrowErrors() throws -> String
 
func cannotThrowErrors() -> String

A throwing function propagates errors that are thrown inside of it to the scope from which it's called.

In the example below, the VendingMachine class has a vend(itemNamed:) method that throws an appropriate ErrorMaquinaExpendedora if the requested item isn't available, is out of stock, or has a cost that exceeds the current deposited amount:

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 ErrorMaquinaExpendedora.seleccionInvalida
        }
 
        guard item.count > 0 else {
            throw ErrorMaquinaExpendedora.agotado
        }
 
        guard item.price <= coinsDeposited else {
            throw ErrorMaquinaExpendedora.fondosInsuficientes(monedasRequeridas: item.price - coinsDeposited)
        }
 
        coinsDeposited -= item.price
 
        var newItem = item
 
        newItem.count -= 1
        inventory[name] = newItem
 
        print("Dispensing \(name)")
    }
}

The implementation of the vend(itemNamed:) method uses guard statements to exit the method early and throw appropriate errors if any of the requirements for purchasing a snack aren't met. Because a throw statement immediately transfers program control, an item will be vended only if all of these requirements are met.

Because the vend(itemNamed:) method propagates any errors it throws, any code that calls this method must either handle the errors—using a do-catch statement, try?, or try!—or continue to propagate them. For example, the buyFavoriteSnack(person:vendingMachine:) in the example below is also a throwing function, and any errors that the vend(itemNamed:) method throws will propagate up to the point where the buyFavoriteSnack(person:vendingMachine:) function is called.

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)
}

In this example, the buyFavoriteSnack(person: vendingMachine:) function looks up a given person's favorite snack and tries to buy it for them by calling the vend(itemNamed:) method. Because the vend(itemNamed:) method can throw an error, it's called with the try keyword in front of it.

Throwing initializers can propagate errors in the same way as throwing functions. For example, the initializer for the PurchasedSnack structure in the listing below calls a throwing function as part of the initialization process, and it handles any errors that it encounters by propagating them to its caller.

struct PurchasedSnack {
    let name: String
 
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
 
        self.name = name
    }
}

Handling Errors Using Do-Catch

You use a do-catch statement to handle errors by running a block of code. If an error is thrown by the code in the do clause, it's matched against the catch clauses to determine which one of them can handle the error.

Here is the general form of a do-catch statement:

do {
    try <#expression#>
    <#statements#>
} catch <#pattern 1#> {
    <#statements#>
} catch <#pattern 2#> where <#condition#> {
    <#statements#>
} catch <#pattern 3#>, <#pattern 4#> where <#condition#> {
    <#statements#>
} catch {
    <#statements#>
}

You write a pattern after catch to indicate what errors that clause can handle. If a catch clause doesn't have a pattern, the clause matches any error and binds the error to a local constant named error. For more information about pattern matching, see Patterns.

For example, the following code matches against all three cases of the ErrorMaquinaExpendedora enumeration.

var vendingMachine = VendingMachine()
 
vendingMachine.coinsDeposited = 8
 
do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
 
    print("Success! Yum.")
} catch ErrorMaquinaExpendedora.seleccionInvalida {
    print("Invalid Selection.")
} catch ErrorMaquinaExpendedora.agotado {
    print("Out of Stock.")
} catch ErrorMaquinaExpendedora.fondosInsuficientes(let monedasRequeridas) {
    print("Insufficient funds. Please insert an additional \(monedasRequeridas) coins.")
} catch {
    print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."

In the above example, the buyFavoriteSnack(person:vendingMachine:) function is called in a try expression, because it can throw an error. If an error is thrown, execution immediately transfers to the catch clauses, which decide whether to allow propagation to continue. If no pattern is matched, the error gets caught by the final catch clause and is bound to a local error constant. If no error is thrown, the remaining statements in the do statement are executed.

The catch clauses don't have to handle every possible error that the code in the do clause can throw. If none of the catch clauses handle the error, the error propagates to the surrounding scope. However, the propagated error must be handled by some surrounding scope. In a nonthrowing function, an enclosing do-catch statement must handle the error. In a throwing function, either an enclosing do-catch statement or the caller must handle the error. If the error propagates to the top-level scope without being handled, you'll get a runtime error.

For example, the above example can be written so any error that isn't a ErrorMaquinaExpendedora is instead caught by the calling function:

func nourish(with item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch is ErrorMaquinaExpendedora {
        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)")
}
// Prints "Couldn't buy that from the vending machine."

In the nourish(with:) function, if vend(itemNamed:) throws an error that's one of the cases of the ErrorMaquinaExpendedora enumeration, nourish(with:) handles the error by printing a message. Otherwise, nourish(with:) propagates the error to its call site. The error is then caught by the general catch clause.

Another way to catch several related errors is to list them after catch, separated by commas. For example:

func eat(item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch ErrorMaquinaExpendedora.seleccionInvalida, ErrorMaquinaExpendedora.fondosInsuficientes, VendingMachineError.agotado {
        print("Invalid selection, out of stock, or not enough money.")
    }
}

The eat(item:) function lists the vending machine errors to catch, and its error text corresponds to the items in that list. If any of the three listed errors are thrown, this catch clause handles them by printing a message. Any other errors are propagated to the surrounding scope, including any vending-machine errors that might be added later.

Converting Errors to Optional Values

You use try? to handle an error by converting it to an optional value. If an error is thrown while evaluating the try? expression, the value of the expression is nil. For example, in the following code x and y have the same value and behavior:

func someThrowingFunction() throws -> Int {
    // ...
}
 
let x = try? someThrowingFunction()
 
let y: Int?
 
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

If someThrowingFunction() throws an error, the value of x and y is nil. Otherwise, the value of x and y is the value that the function returned. Note that x and y are an optional of whatever type someThrowingFunction() returns. Here the function returns an integer, so x and y are optional integers.

Using try? lets you write concise error handling code when you want to handle all errors in the same way. For example, the following code uses several approaches to fetch data, or returns nil if all of the approaches fail.

  func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
 
    if let data = try? fetchDataFromServer() { return data }
 
    return nil
}

Disabling Error Propagation

Sometimes you know a throwing function or method won't, in fact, throw an error at runtime. On those occasions, you can write try! before the expression to disable error propagation and wrap the call in a runtime assertion that no error will be thrown. If an error actually is thrown, you'll get a runtime error.

For example, the following code uses a loadImage(atPath:) function, which loads the image resource at a given path or throws an error if the image can't be loaded. In this case, because the image is shipped with the application, no error will be thrown at runtime, so it's appropriate to disable error propagation.

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

Specifying Cleanup Actions

You use a defer statement to execute a set of statements just before code execution leaves the current block of code. This statement lets you do any necessary cleanup that should be performed regardless of how execution leaves the current block of code—whether it leaves because an error was thrown or because of a statement such as return or break. For example, you can use a defer statement to ensure that file descriptors are closed and manually allocated memory is freed.

A defer statement defers execution until the current scope is exited. This statement consists of the defer keyword and the statements to be executed later. The deferred statements may not contain any code that would transfer control out of the statements, such as a break or a return statement, or by throwing an error. Deferred actions are executed in the reverse of the order that they're written in your source code. That is, the code in the first defer statement executes last, the code in the second defer statement executes second to last, and so on. The last defer statement in source code order executes first.

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) is called here, at the end of the scope.
    }
}

The above example uses a defer statement to ensure that the open(_:) function has a corresponding call to close(_:).