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)