Error handling - RaduG/swift_learning GitHub Wiki

Overview

In Swift, objects that implement the Error can be thrown. Enums are particularly good for this.

enum ATMError: Error {
  case invalidPin
  case cardNotReadable
  case insufficientFunds(requestedAmount: Int, balance: Int)
}

throw ATMError.insufficientFunds(requestedAmount: 100, balance: 80)

However, compared to other languages, throw is closer to return in terms of performance as raising an error does not require the frame stack to be unwind.

Functions that throw errors

Functions that throw errors must be declared as such by including the keyword throws before the return type declaration.

func someFunc(a: Int, b: Double) throws -> String

Handling throwing code

To execute a function that can throw an error, try must always be used:

enum Errors: Error {
  case evenNumber
}

func functionThatThrows(_ n: Int) throws -> Int {
  if n % 2 == 0 {
    throw Errors.evenNumber
  }

  return n
}

let n = try functionThatThrows(100) // without try, a compiler error is triggered

It is possible to automatically convert the result of a try expression into an Optional using try?:

let n = try? functionThatThrows(100)
if n == nil {
    print("Error thrown!")
}

It is also possible to disable error propagation by using try!, which will in turn cause a runtime error if an error is actually thrown by that expression.

let n = try! functionThatThrows(101) // we know that this will not raise an error

Do-catch

Errors can be caught by using the do-catch statement together with try expressions. The catch clauses don't have to be exhaustive (unlike case statements).

do {
  try expression1
  // other statements
} catch ErrorType1, ErrorType2 {
  //
} catch ErrorType1 where ... {
  //
} catch {
  // catch all / fallback
}

Note: There is no single-statement equivalent to try-except-finally - instead, use a defer block. Defer blocks are always executed before the function returns regardless of the result (similar to finally in Python).

More extensive example:

class BankAccount {
  enum BankAccountError: Error {
    case insufficientFunds
  }

  let accountNumber: String
  let sortCode: String
  private var internalBalance: Double
  var balance: Double {
    internalBalance
  }

  init(accountNumber: String, sortCode: String, balance: Double) {
    self.accountNumber = accountNumber
    self.sortCode = sortCode
    self.internalBalance = balance
  }

  convenience init(accountNumber: String, sortCode: String) {
    self.init(accountNumber: accountNumber, sortCode: sortCode, balance: 0)
  }

  func credit(amount: Double) {
    internalBalance += amount
  }

  func debit(amount: Double) throws {
    if internalBalance < amount {
      throw BankAccountError.insufficientFunds
    }

    internalBalance -= amount
  }

  func makeCard() -> Card {
    return Card(
      number: accountNumber + sortCode,
      pin: String(Int.random(in: 1111...9999)),
      bankAccount: self
    )
  }

  func toString() -> String {
    return "BankAccount(accountNumber=\(accountNumber), sortCode=\(sortCode), balance=\(balance))"
  }
}

struct Card {
  let number: String
  let pin: String
  let bankAccount: BankAccount

  func authenticate(pin: String) -> Bool {
    return self.pin == pin
  }

  func debit(amount: Double) -> Bool {
    do {
      try bankAccount.debit(amount: amount)
    } catch {
      return false
    }

    return true
  }

  func balance() -> Double {
    return bankAccount.balance
  }

  func toString() -> String {
    return "Card(number=\(number), pin=\(pin), bankAccount=\(bankAccount.toString()))"
  }
}

class ATM {
  enum ATMError: Error {
    case notReady
    case invalidPin
    case invalidOperation
    case notEnoughCash
    case insufficientFunds(requestedAmount: Double, balance: Double)
  }

  enum ATMOperation: String, CaseIterable {
    case withdraw = "1"
    case cancel = "2"
  }

  let availableCash: Double

  init(funds availableCash: Double) {
    self.availableCash = availableCash
  }

  func use(card: Card) throws {
    print("ATM Ready, please insert card")
    print("\(card.toString()) inserted!")
    print("Please insert PIN: ")
    let pin = readLine()!

    guard card.authenticate(pin: pin) else {
      print("Incorrect PIN!")
      throw ATMError.invalidPin
    }
    
    print("PIN correct!")

    print("Please select operation type:")
    for op in ATM.ATMOperation.allCases {
      print("\(op.rawValue): \(op)")
    }

    guard let op = ATM.ATMOperation(rawValue: readLine()!) else {
      print("Invalid operation!")
      throw ATMError.invalidOperation
    }

    if op == ATM.ATMOperation.withdraw {
      print("What amount would you like to withdraw?")
      let amount = Double(readLine()!)!

      try self.withdraw(card: card, amount: amount)
    } else if op == ATM.ATMOperation.cancel {
      print("OK, bye!")
    }
  }

  func withdraw(card: Card, amount: Double) throws {
    if amount > availableCash {
      throw ATMError.notEnoughCash
    }

    if card.debit(amount: amount) {
      print("Withdrawing \(amount) from card \(card)")
    } else {
      throw ATMError.insufficientFunds(requestedAmount: amount, balance: card.balance())
    }
  }
}


let bankAccount = BankAccount(accountNumber: "1234567", sortCode: "121314", balance: 100)
let card = bankAccount.makeCard()
print(card)
let atm = ATM(funds: 90)

try atm.use(card: card)