DEV Community

Naveen Ragul B
Naveen Ragul B

Posted on • Updated on

Swift - Error Handling, Assertions and PreConditions

Error handling can be used to respond to error conditions your program may encounter during execution.

  • A function indicates that it can throw an error by including the throws keyword in its declaration. When you call a function that can throw an error, you prepend the try keyword to the expression. Swift automatically propagates errors out of their current scope until they’re handled by a catch clause.

  • A do statement creates a new containing scope, which allows errors to be propagated to one or more catch clauses.

syntax :

do {
    try canThrowAnError()
    // no error was thrown
} catch {
    // an error was thrown
}
Enter fullscreen mode Exit fullscreen mode
  • Errors are represented by values of types that conform to the Error protocol. This empty protocol indicates that a type can be used for error handling.

  • Enumerations are well suited to modelling a group of related error conditions, with associated values allowing for additional information about the nature of an error to be communicated. throw statement can be used to throw an error.

example :

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}
Enter fullscreen mode Exit fullscreen mode
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Enter fullscreen mode Exit fullscreen mode

Handling Errors

There are four ways to handle errors in Swift:

  1. Propagate the error from a function to the code that calls that function.
  2. Handle the error using a do-catch statement.
  3. Handle the error as an optional value.
  4. Assert that the error will not occur.

Propagating Errors Using Throwing Functions

  • Throwing function, method, initializer should be marked with throws.

  • Only throwing functions can propagate errors. Any errors thrown inside a nonthrowing function must be handled inside the function.

example :

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)")
    }
}
Enter fullscreen mode Exit fullscreen mode

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.


Handling Errors Using Do-Catch

do-catch statement can be used to handle errors by executing 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.

example :

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    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).")
}
Enter fullscreen mode Exit fullscreen mode
  • 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.

example :

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)")
}
Enter fullscreen mode Exit fullscreen mode

Converting Errors to Optional Values

try? can be used 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.

example :

func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()
Enter fullscreen mode Exit fullscreen mode

Disabling Error Propagation

If you are sure that a throwing function or method won’t throw an error at runtime, then write try! before the expression to disable error propagation. If an error actually is thrown, you’ll get a runtime error.

example :

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

Specifying Cleanup Actions

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.

  • The deferred statements may not contain transfer control 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.

example:

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.
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Assertions and Preconditions

Assertions and preconditions are checks that happen at runtime. You use them to make sure an essential condition is satisfied before executing any further code. If the Boolean condition in the assertion or precondition evaluates to true, code execution continues as usual. If the condition evaluates to false, the current state of the program is invalid; code execution ends, and your app is terminated.

  • assertions and preconditions aren’t used for recoverable or expected errors. Because a failed assertion or precondition indicates an invalid program state, there’s no way to catch a failed assertion.

  • They are used to Stop the execution as soon as an invalid state is detected also helps to limit the damage caused by that invalid state.

  • Assertions are checked only in debug builds, but preconditions are checked in both debug and production builds.

  • Assertions help you find mistakes and incorrect assumptions during development, and preconditions help you detect issues in production.

example :

let age = -3
assert(age >= 0, "A person's age can't be less than zero.")
Enter fullscreen mode Exit fullscreen mode

or

if age > 10 {
    print("You can ride the roller-coaster or the ferris wheel.")
} else if age >= 0 {
    print("You can ride the ferris wheel.")
} else {
    assertionFailure("A person's age can't be less than zero.")
}
Enter fullscreen mode Exit fullscreen mode
precondition(index > 0, "Index must be greater than zero.")
Enter fullscreen mode Exit fullscreen mode

Top comments (0)