Error handling : 프로그램에서 error 에 대응하고 recovering 하는 것.

Representing and Throwing Errors

Swift 에서 에러는 Error 프로토콜을 conform 하는 type 에 의해 표현됨. 빈 Error 프로토콜을 통해 에러 핸들링에 사용될 수 있는 타입을 나타냄.

주로 Swift enum 은 관련된 에러 조건을 모델링하는데 적합함.

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}

Error 를 throw 해서 플로우 상 더 이상 진행할 수 없는 예상하지 못한 무언가가 발생했음을 나타냄. throw 문을 통해 에러를 던짐.

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

Handling Errors

에러가 던져지면, 해당 에러를 처리해야 하는 곳이 필요함.

함수가 에러를 던지는 경우, 프로그램의 플로우를 바꾸기 때문에 에러를 던질 수 있는 곳을 명시하는 것이 중요함. try / try? / try! 를 에러를 던질 수 있는 function/method/initializer 전에 사용

Swift 의 error handling 은 다른 언어의 exception handling 과 비슷함. 다른 언어와 다른 점은 Swift 의 error handling 은 연산이 많이 필요할 수 있는 call stack 을 unwinding 하는 작업을 포함하지 않음. 따라서 throw 문의 성능은 return 문과 거의 비슷할 정도

Propagating Errors Using Throwing Functions

Function 정의 다음에 throw 키워드를 써서 function/method/initializer 가 error 를 던지는 것을 표현 가능. throws 로 마킹된 함수는 throwing function 이라고 불림. Return type 이 있는 function 은 return arrow (->) 전에 throws 키워드를 붙임

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

Throwing function 은 내부에서 호출된 scope 로 error 를 전파함

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

let favoriteSnacks = [
    "Alice": "Chips",
    "Bob": "Licorice",
    "Eve": "Pretzels",
]

func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    // vend(itemNamed:) method 가 던진 error 는 buyFavoriteSnack() 가 호출된 곳으로 propagate 됨
    try vendingMachine.vend(itemNamed: snackName)
}

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

Handling Errors Using Do-Catch

do-catch 문을 사용해서 에러를 handling 할 수 있음

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

catch 문은 가능한 모든 에러를 핸들링해야 할 필요는 없음. 만약 매칭되는 catch 가 없다면 상위 scope 로 propagate. 에러가 handling 되지 않고 top-level scope 로 propagate 됐다면 runtime error 발생

func nourish(with item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch is VendingMachineError {
        print("Couldn't buy that from the vending machine.")
    }
    // catch 안 된 에러는 어디선가 handling 되어야 함
}

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."

Converting Errors to Optional Values

try? 를 써서 optional 값으로 바꿔서 error 를 handling 하는 방법도 있음. 만약 에러가 try? 문에서 던져졌다면 nil 값이 리턴됨.

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

let x = try? someThrowingFunction()

// x 와 같은 동작
let y: Int?

do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

Disabling Error Propagation

Throwing function 이 error 를 runtime 에 던지지 않을 것이라는 것이 확실한 경우도 있음. 이런 경우에는 try! 를 통해 호출을 runtime assertion 에 감싸서 에러가 throw 되지 않을 것임을 나타내는 방법도 있음.

// 항상 error 를 던지지 않을 것임을 알 때 사용
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

Specifying the Error Type

위의 예제들에서 throw 된 에러들은 Error 프로토콜을 conform 하는 어떤 타입이든 될 수 있음. 이런 접근 방식은 코드가 실행되고 있을 때, 특히 error 가 다른 어딘가에서 propagate 됐을 때 모든 에러를 알 수 없는 실제 상황을 반영함. 또한 에러가 시간에 따라 달라질 수 있음을 반영하기도 함. (e.g. 의존하고 있는 라이브러리의 버전이 업그레이드 돼서 새로운 에러가 throw 될 수 있음)

대부분의 Swift code 는 던지는 에러의 타입을 명시하지 않음. 하지만 특별한 경우에 throw 되는 에러가 특정 타입을 따르도록 제약을 추가할 수 있음.

Typed throw 문법 : 에러의 타입을 throw 뒷부분에 명시

enum StatisticsError: Error {
    case noRatings
    case invalidRating(Int)
}

// 오직 `StatisticsError` 만 throw 할 것임을 명시
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]!)
}

Never 로 절대로 return 하지 않는 함수를 작성할 수 있음. 아래 함수는 Never 타입의 값을 생성하는 것이 불가능하기 때문에 throw 를 할 수 없음

func nonThrowingFunction() throws(Never) {
  // ...
}

함수의 error type 을 명시하는 것에 추가로 do-catch 문에도 특정 error type 을 쓸 수 있음

let ratings = []

do throws(StatisticsError) { // 오직 StatisticsError 만 throw 할 것임을 명시함
    try summarize(ratings)
} catch {
    switch error {
    case .noRatings:
        print("No ratings available")
    case .invalidRating(let rating):
        print("Invalid rating: \(rating)")
    }
}
// Prints "No ratings available".

// 이미 특정 타입의 에러만 throw 하고 있다면 typed throw 를 추론하고, 더 간결하게 작성 가능
let ratings = []
do {
    try summarize(ratings)
} catch {
    switch error {
    case .noRatings:
        print("No ratings available")
    case .invalidRating(let rating):
        print("Invalid rating: \(rating)")
    }
}
// Prints "No ratings available".

Specifying Cleanup Actions

현재 코드 block 실행이 끝나기 전에 실행할 코드들을 defer 문을 사용해서 실행함. defer 는 코드가 어떤 방식으로 (error thrown/return/break) 끝나든 항상 실행해야 하는 cleanup 코드를 실행할 수 있게 해줌.

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