$30 off During Our Annual Pro Sale. View Details »

Straying From the Happy Path: Taking Control of Errors in Swift

Straying From the Happy Path: Taking Control of Errors in Swift

Have you ever seen a code comment saying “this should never happen”? Have you ever wondered what would happen if it did? Swift has a diverse set of error-handling capabilities, from using throw to send errors up the stack to using a Result to handle errors in asynchronous methods. In this talk, we’ll look at the landscape of handling errors in Swift, create new ways of expressing and handling them, and show how even the most impossible code-level situations can have 100% test coverage. By the end, you’ll be taking control of the errors in your code, instead of letting them take control of you.

Jeff Kelley

January 10, 2020
Tweet

More Decks by Jeff Kelley

Other Decks in Programming

Transcript

  1. Straying From the Happy Path
    Taking Control of Errors in Swift
    Jeff Kelley (@SlaunchaMan)
    #CodeMash 2020, January 10th, 2020

    View Slide

  2. Agenda
    The History of Error-Handling: Objective-C and Swift
    The Swift Error Type
    Testing Errors
    Error Handling Through Types: Combine and SwiftUI
    Jeff Kelley |

    View Slide

  3. Agenda
    The History of Error-Handling: Objective-C and Swift
    The Swift Error Type
    Testing Errors
    Error Handling Through Types: Combine and SwiftUI
    Jeff Kelley |

    View Slide

  4. Error Handling in Objective-C
    Error Pointers
    Objective-C used a pointer to an NSError object to vend errors
    back to the calling code. When the removeItemAtPath:error:
    method finishes, if an error occurred, success would be NO
    and error might have a pointer to an NSError in it.
    NSError *error;
    BOOL success =
    [[NSFileManager defaultManager]
    removeItemAtPath:@"/tmp/dontneedthisanymore"
    error:&error];
    So, how do we handle this kind of error?
    Jeff Kelley |

    View Slide

  5. Error Handling in Objective-C
    Error Pointers
    You might think to do it this way:
    NSError *error;
    BOOL success =
    [[NSFileManager defaultManager]
    removeItemAtPath:@"/tmp/dontneedthisanymore"
    error:&error];
    if (error != nil) {
    // Handle the error
    }
    Jeff Kelley |

    View Slide

  6. Error Handling in Objective-C
    Error Pointers
    But in reality, you need to do it this way:
    NSError *error;
    BOOL success =
    [[NSFileManager defaultManager]
    removeItemAtPath:@"/tmp/dontneedthisanymore"
    error:&error];
    if (!success) {
    if (error != nil) {
    // Handle the error
    }
    }
    The value in error is not sufficient for determining the result of
    the operation.
    Jeff Kelley |

    View Slide

  7. Error Handling in Objective-C
    Completion Handlers
    When writing asynchronous code, we often use completion
    handlers to convey the results of the operation.
    HKHealthStore *store = [[HKHealthStore alloc] init];
    [store startWatchAppWithWorkoutConfiguration:config
    completion:^(BOOL success,
    NSError * _Nullable error) {
    if (!success) {
    NSLog(@"Error starting workout: %@", [error localizedDescription]);
    // Alert the user that their workout didn’t start
    }
    }];
    This pattern is so common, we have a type for it in Swift.
    Jeff Kelley |

    View Slide

  8. Errors in Swift
    Result
    Result is perfect for asynchronous code—it clearly defines the
    type you’ll have in both the success and failure cases.
    /// A value that represents either a success or a failure, including an
    /// associated value in each case.
    public enum Result where Failure : Error {
    /// A success, storing a `Success` value.
    case success(Success)
    /// A failure, storing a `Failure` value.
    case failure(Failure)

    }
    Jeff Kelley |

    View Slide

  9. Result in Practice
    Network Requests
    A typical place you’ll see a Result is in fetching something from
    the network:
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    performRequest(url) { result in
    switch result {
    case .success(let data):
    // Parse data into model object
    case .failure(let error):
    // Handle error
    }
    }
    Jeff Kelley |

    View Slide

  10. Result in Practice
    map()
    We can use map(_:) to transform the success case of a Result
    into a new value.
    func map(
    _ transform: (Success) -> NewSuccess
    ) -> Result
    1 If you want to transform the failure type, there’s a corresponding mapError(_:) method.
    Jeff Kelley |

    View Slide

  11. Result in Practice
    map(_:)
    A common use of map(_:) is to transform data from the
    network into a model object:
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    performRequest(url) { result in
    // result is a Result
    let mappedResult = result.map(CatFact.init)
    // mappedResult is a Result
    switch result.map { {
    case .success(let catFact):
    // Handle parsed cat fact
    case .failure(let error):
    // Handle error
    }
    }
    Jeff Kelley |

    View Slide

  12. Error-Handling is Unenforceable.
    You can lead a developer to error-handling APIs, but you can’t make them use them.
    Photo by Brett Jordan on Unsplash

    View Slide

  13. Result in Actual Practice
    How many times have you written code like this?
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    performRequest(url) { result in
    switch result {
    case .success(let data):
    // Parse data into model object
    case .failure(let error):
    // TODO: Handle Error
    break
    }
    }
    How many times do you forget about that TODO?
    Jeff Kelley |

    View Slide

  14. Result in Actual Practice
    Or even have the TODO?
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    performRequest(url) { result in
    if case .success(let data) = result {
    // Handle data
    }
    }
    Jeff Kelley |

    View Slide

  15. Objective-C Error Handling in Practice
    NSError *error;
    BOOL success =
    [[NSFileManager defaultManager]
    removeItemAtPath:@"/tmp/dontneedthisanymore"
    error:&error];
    if (success) {
    // Proceed with what you were doing
    }
    else {
    // TODO: Handle error.
    NSLog(@"Error: %@", error.localizedDescription);
    }
    I mean, it could be worse. At least we’re logging the error.
    Jeff Kelley |

    View Slide

  16. Objective-C Error Handling in Practice
    NSError *error;
    BOOL success =
    [[NSFileManager defaultManager]
    removeItemAtPath:@"/tmp/dontneedthisanymore"
    error:&error];
    if (success) {
    // Proceed with what you were doing
    }
    Jeff Kelley |

    View Slide

  17. Objective-C Error Handling in Practice
    BOOL success =
    [[NSFileManager defaultManager]
    removeItemAtPath:@"/tmp/dontneedthisanymore"
    error:NULL];
    if (success) {
    // Proceed with what you were doing
    }
    Jeff Kelley |

    View Slide

  18. Agenda
    The History of Error-Handling: Objective-C and Swift
    The Swift Error Type
    Testing Errors
    Error Handling Through Types: Combine and SwiftUI
    Jeff Kelley |

    View Slide

  19. Swift Errors
    Objective-C
    Remember the removeItemAtPath:error: method we used in
    Objective-C?
    - (BOOL)removeItemAtPath:(NSString *)path
    error:(NSError * _Nullable *)error;
    Jeff Kelley |

    View Slide

  20. Swift Errors
    Swift
    Here’s how that method appears in Swift. Now it’s
    removeItem(atPath:).
    func removeItem(atPath path: String) throws
    Notice that the error parameter is gone—where did it go?
    Jeff Kelley |

    View Slide

  21. Swift Errors
    If we just call removeItem(atPath:), this won’t compile.
    FileManager.default
    .removeItem(atPath: "/tmp/dontneedthisanymore")
    Jeff Kelley |

    View Slide

  22. Swift Errors
    If we just call removeItem(atPath:), this won’t compile.
    FileManager.default
    .removeItem(atPath: "/tmp/dontneedthisanymore")
    //
    !
    Call can throw but is not marked with 'try'
    Jeff Kelley |

    View Slide

  23. Swift Errors
    Any method marked throws needs us to try calling it first:
    do {
    try FileManager.default
    .removeItem(atPath: "/tmp/dontneedthisanymore")
    }
    catch {
    print(error.localizedDescription)
    }
    There’s our error parameter.
    Jeff Kelley |

    View Slide

  24. The Swift Error Type
    public protocol Error {
    }
    Jeff Kelley |

    View Slide

  25. The Swift Error Type
    Custom Error Types
    If you know specific error cases you can run into, you can
    create an enum to represent these states.
    enum CatFactParsingError: Error {
    case invalidData
    case unknown
    case actuallyADogFact
    }
    Jeff Kelley |

    View Slide

  26. The Swift Error Type
    Catching Specific Errors
    And once you have these cases, you can catch them:
    do {
    try parseCatFact()
    }
    catch CatFactParsingError {
    print("Bad data!")
    }
    catch {
    // Gotta catch ‘em all!
    print("Some other error: \(error.localizedDescription)")
    }
    Jeff Kelley |

    View Slide

  27. Swift Errors
    Just like Result, you can simply ignore the errors.
    try? FileManager.default
    .removeItem(atPath: "/tmp/dontneedthisanymore")
    // or
    try! FileManager.default
    .removeItem(atPath: "/tmp/dontneedthisanymore")
    Jeff Kelley |

    View Slide

  28. throws
    You can of course use throws in your own code:
    func deleteTempoararyData() throws {
    try FileManager.default
    .removeItem(atPath: "/tmp/dontneedthisanymore")
    }
    Jeff Kelley |

    View Slide

  29. throws
    You can also use this with methods that return values:
    func retrieveTemporaryData() throws -> String? {
    let path = "/tmp/dontneedthisanymore"
    let temporaryData = try String(contentsOfFile: path)
    try FileManager.default.removeItem(atPath: path)
    return temporaryData
    }
    Jeff Kelley |

    View Slide

  30. Advanced throws
    We use map(_:) all the time without try, thanks to the rethrows
    keyword:
    func map(
    _ transform: (Self.Element) throws -> T
    ) rethrows -> [T]
    Jeff Kelley |

    View Slide

  31. Advanced throws
    You can write this yourself, just use try inside of your rethrows
    method:
    extension Collection {
    public func map(
    _ transform: (Element) throws -> T
    ) rethrows -> [T] {
    var result: [T] = []
    for item in self {
    let transformed = try transform(item)
    result.append(transformed)
    }
    return result
    }
    }
    Jeff Kelley |

    View Slide

  32. The Error Type
    The Error protocol has a localizedDescription property:
    protocol Error {
    var localizedDescription: String { get }
    }
    You can use this property when you need to display the error:
    CatLog("Error parsing cat fact: \(error.localizedDescription)")
    Jeff Kelley |

    View Slide

  33. Effective Swift Errors
    Logging
    You may find yourself writing code like this a lot:
    healthStore.add(samples, to: workout) { success, error in
    if let error = error {
    Log("Error adding samples to workout: " +
    error.localizedDescription)
    }
    }
    Jeff Kelley |

    View Slide

  34. Effective Swift Errors
    Logging
    The pattern involves logging the error if there is one with some
    additional context.
    if let error = error {
    Log("Error : " +
    error.localizedDescription)
    }
    Jeff Kelley |

    View Slide

  35. Effective Swift Errors
    Logging
    We can use an extension on Error to wrap this code:
    extension Error {
    func log(context: String,
    filename: String = #file,
    lineNumber: Int = #line) {
    Log("Error \(context): \(localizedDescription)",
    type: .error,
    filename: filename,
    lineNumber: lineNumber)
    }
    }
    Jeff Kelley |

    View Slide

  36. Effective Swift Errors
    Logging
    Here it is in use:
    healthStore.add(samples, to: workout) { success, error in
    error?.log(context: "adding samples to workout")
    }
    Jeff Kelley |

    View Slide

  37. Agenda
    The History of Error-Handling: Objective-C and Swift
    The Swift Error Type
    Testing Errors
    Error Handling Through Types: Combine and SwiftUI
    Jeff Kelley |

    View Slide

  38. Testing Result
    The Happy Path
    Since Result is Equatable when its Success and Failure types
    are Equatable, we can use XCTAssertEqual(_:_:_:file:line:)
    class ResultTests: XCTestCase {
    func testTheHappyPath() {
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    let expectation = self.expectation("The request finishes")
    performRequest(url) { result in
    XCTAssertEqual(result, .success)
    expectation.fulfill()
    }
    waitForExpectations(timeout: 2)
    }
    }
    Jeff Kelley |

    View Slide

  39. Testing Result
    The Failure Case
    We can do the same thing for the failure case:
    class ResultTests: XCTestCase {
    func testTheFailurePath() {
    let url = URL(string: "https://catfactsapi.cool/api/dogfactoftheday")!
    let expectation = self.expectation("The request finishes")
    performRequest(url) { result in
    XCTAssertEqual(result, .failure(NetworkError.notFound))
    expectation.fulfill()
    }
    waitForExpectations(timeout: 2)
    }
    Jeff Kelley |

    View Slide

  40. fatalError()
    If you can’t handle an error, sometimes the right thing to do is
    crash:
    do {
    try somethingThatCouldThrow()
    }
    catch {
    fatalError(error.localizedDescription)
    }
    Jeff Kelley |

    View Slide

  41. Testing fatalError()
    Jeff Kelley |

    View Slide

  42. Testing throws
    Consider the following code:
    enum ThrowingError: Error {
    case passedFalse
    }
    func throwIfPassedFalse(
    _ trueOrFalse: Bool
    ) throws {
    guard trueOrFalse else {
    throw ThrowingError.passedFalse
    }
    }
    Jeff Kelley |

    View Slide

  43. Testing throws
    All we need to do is assert that the error is thrown.
    func testThrowingError() {
    XCTAssertThrowsError(
    try throwIfPassedFalse(false),
    "Expected method to throw") { error in
    XCTAssertEqual(
    error as? ThrowingError,
    ThrowingError.passedFalse)
    }
    }
    Jeff Kelley |

    View Slide

  44. Testing fatalError()
    Replacing the call to fatalError()
    If we put this method in our app code, it’ll take precedence
    over the original fatalError():
    func fatalError(
    _ message: @autoclosure () -> String = "",
    file: StaticString = #file,
    line: UInt = #line
    ) -> Never {
    Swift.fatalError(message,
    file: file,
    line: line)
    }
    Jeff Kelley |

    View Slide

  45. Testing fatalError()
    Replacing fatalError() With a Closure
    func fatalError(
    _ message: @autoclosure () -> String = "",
    file: StaticString = #file,
    line: UInt = #line
    ) -> Never {
    FatalErrorUtilities.fatalErrorClosure(message(), file, line)
    }
    struct FatalErrorUtilities {
    typealias FatalErrorClosure = (String, StaticString, UInt) -> Never
    fileprivate static var fatalErrorClosure = defaultFatalErrorClosure
    private static let defaultFatalErrorClosure = {
    (message: String, file: StaticString, line: UInt) -> Never in
    Swift.fatalError(message, file: file, line: line)
    }
    }
    Jeff Kelley |

    View Slide

  46. Testing fatalError()
    Swapping Out the Implementation
    extension FatalErrorUtilities {
    internal static func replaceFatalError(
    closure: @escaping FatalErrorClosure
    ) {
    fatalErrorClosure = closure
    }
    internal static func restoreFatalError() {
    fatalErrorClosure = defaultFatalErrorClosure
    }
    }
    Jeff Kelley |

    View Slide

  47. Testing fatalError()
    Expect the Error
    extension XCTestCase {
    func expectFatalError(file: StaticString = #file,
    line: UInt = #line,
    testcase: @escaping () -> Void) {
    let expectation = self.expectation(description: "expecting fatal error")
    FatalErrorUtilities.replaceFatalError { (_, _, _) -> Never in
    expectation.fulfill()
    self.unreachable()
    }
    DispatchQueue.global().async(execute: testcase)
    waitForExpectations(timeout: 2) { _ in
    FatalErrorUtilities.restoreFatalError()
    }
    }
    func unreachable() -> Never {
    while true { RunLoop.current.run() }
    }
    }
    Jeff Kelley |

    View Slide

  48. Testing fatalError()
    Putting it All Together
    Now we can write this test, and it’ll pass without crashing!

    func testCrashing() {
    expectFatalError {
    fatalErrorIfPassedFalse(false)
    }
    }
    Jeff Kelley |

    View Slide

  49. Testing fatalError()
    Jeff Kelley |

    View Slide

  50. When to Use fatalError()
    One common use of fatalError() is to stop execution if you get
    into a situation you shouldn’t be in:
    func tableView(
    _ tableView: UITableView,
    cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(
    withIdentifier: "cell")
    as? MyTableViewCell
    else { fatalError() }
    return cell
    }
    Jeff Kelley |

    View Slide

  51. Swift Assertion Methods
    Assert.swift in the Swift Standard Library
    Assert Type Debug Release Unchecked
    fatalError
    ✅ ✅
    precondition
    ✅ ✅

    assert

    ❌ ❌
    Jeff Kelley |

    View Slide

  52. Swift Assertion Methods
    assert() and precondition()
    These are functionally equivalent:
    if (!someCondition) {
    assertionFailure()
    }
    assert(someCondition)
    Jeff Kelley |

    View Slide

  53. Effective Swift Errors
    ProgrammerError
    We can use throws and a custom Error type to wrap the code
    for dequeueing table view cells:
    extension UITableView {
    enum ProgrammerError: Error {
    case noCellReturned
    }
    func dequeue(
    _ type: T.Type,
    identifier reuseIdentifier: String
    ) throws -> T {
    guard let cell = dequeueReusableCell(
    withIdentifier: reuseIdentifier) as? T
    else { throw ProgrammerError.noCellReturned }
    return cell
    }
    }
    Jeff Kelley |

    View Slide

  54. Effective Swift Errors
    ProgrammerError
    Now, we can use this in our code:
    func tableView(
    _ tableView: UITableView,
    cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
    do {
    return try tableView.dequeue(
    MyTableViewCell.self,
    identifier: "cell")
    }
    catch {
    fatalError(error.localizedDescription)
    }
    }
    Jeff Kelley |

    View Slide

  55. Effective Swift Errors
    Passing Errors to fatalError() and Friends
    Let’s write another replacement for fatalError() that takes an
    Error instead:
    func fatalError(
    _ error: Error,
    file: StaticString = #file,
    line: UInt = #line
    ) -> Never {
    fatalError(error.localizedDescription,
    file: file,
    line: line)
    }
    Jeff Kelley |

    View Slide

  56. Effective Swift Errors
    Passing Errors to fatalError() and Friends
    Now we can just pass the error through directly:
    func tableView(
    _ tableView: UITableView,
    cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
    do {
    return try tableView.dequeue(
    MyTableViewCell.self,
    identifier: "cell")
    }
    catch {
    fatalError(error)
    }
    }
    Jeff Kelley |

    View Slide

  57. Effective Swift Errors
    Testing Specific Error Conditions
    We can use this in our tests, too:
    func expectFatalError(expectedError: T,
    file: StaticString = #file,
    line: UInt = #line,
    testcase: @escaping () -> Void) where T: Equatable {
    let expectation = self.expectation(description: "expecting fatal error")
    var assertionError: T? = nil
    FatalErrorUtilities.replaceFatalError { error, _, _ in
    assertionError = error as? T
    expectation.fulfill()
    self.unreachable()
    }
    DispatchQueue.global().async(execute: testcase)
    waitForExpectations(timeout: 2) { _ in
    XCTAssertEqual(assertionError,
    expectedError,
    file: file,
    line: line)
    FatalErrorUtilities.restoreFatalError()
    }
    }
    Jeff Kelley |

    View Slide

  58. “But This Should Never Happen”
    __block void (^recursiveBlock)(void) = ^{
    NSLog(@"foo");
    recursiveBlock();
    };
    recursiveBlock();
    This code crashed on Release builds, not Debug builds.
    Jeff Kelley |

    View Slide

  59. “But This Should Never Happen”
    if (someObject != NULL && someObject->someValue != 0) {
    // Do something very impressive
    }
    On a compiler without short-circuit evaluation, this crashed
    when someObject was NULL.
    Jeff Kelley |

    View Slide

  60. Agenda
    The History of Error-Handling: Objective-C and Swift
    The Swift Error Type
    Testing Errors
    Error Handling Through Types: Combine and SwiftUI
    Jeff Kelley |

    View Slide

  61. Combine
    Values That Change Over Time
    public protocol Publisher {
    /// The kind of values published by this publisher.
    associatedtype Output
    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    associatedtype Failure : Error
    /// This function is called to attach the specified `Subscriber` to this
    /// `Publisher` by `subscribe(_:)`
    ///
    /// - SeeAlso: `subscribe(_:)`
    /// - Parameters:
    /// - subscriber: The subscriber to attach to this `Publisher`.
    /// once attached it can begin to receive values.
    func receive(subscriber: S) where
    S : Subscriber,
    Self.Failure == S.Failure,
    Self.Output == S.Input
    }
    Jeff Kelley |

    View Slide

  62. Combine
    URLSession’s DataTaskPublisher
    extension URLSession {
    public struct DataTaskPublisher : Publisher {
    /// The kind of values published by this publisher.
    public typealias Output = (data: Data, response: URLResponse)
    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    public typealias Failure = URLError
    /// This function is called to attach the specified `Subscriber` to this
    /// `Publisher` by `subscribe(_:)`
    ///
    /// - SeeAlso: `subscribe(_:)`
    /// - Parameters:
    /// - subscriber: The subscriber to attach to this `Publisher`.
    /// once attached it can begin to receive values.
    public func receive(subscriber: S) where
    S : Subscriber,
    S.Failure == URLSession.DataTaskPublisher.Failure,
    S.Input == URLSession.DataTaskPublisher.Output
    }
    }
    Jeff Kelley |

    View Slide

  63. Combine Operators
    let url = URL(string: "https://batcave.info/enemies/joker")!
    let publisher = URLSession.shared.dataTaskPublisher(for: url)
    let enemyPublisher: AnyPublisher = publisher.tryMap {
    let decoder = JSONDecoder()
    let enemy = try decoder.decode(Enemy.self, from: $0.data)
    return enemy
    }
    .eraseToAnyPublisher()
    Jeff Kelley |

    View Slide

  64. Combine Publishers
    The Never Type
    NotificationCenter
    .default
    .publisher(for: UIResponder.keyboardWillShowNotification)
    .sink { notification in
    // Process notification
    }
    Jeff Kelley |

    View Slide

  65. Combine Publishers
    The Never Type
    extension NotificationCenter {
    /// A publisher that emits elements when broadcasting notifications.
    public struct Publisher : Publisher {
    /// The kind of values published by this publisher.
    public typealias Output = Notification
    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    public typealias Failure = Never
    }
    }
    Jeff Kelley |

    View Slide

  66. SwiftUI and Errors
    public protocol ObservableObject : AnyObject {
    /// The type of publisher that emits before the object has changed.
    associatedtype ObjectWillChangePublisher : Publisher =
    ObservableObjectPublisher
    where Self.ObjectWillChangePublisher.Failure == Never
    /// A publisher that emits before the object has changed.
    var objectWillChange: Self.ObjectWillChangePublisher { get }
    }
    Jeff Kelley |

    View Slide

  67. SwiftUI and Errors
    Enforcing Error-Handling Through Types
    struct CatView: View {
    @ObservedObject var cat: Cat
    static let lengthFormatter: LengthFormatter = {
    let formatter = LengthFormatter()
    formatter.isForPersonHeightUse = false // it’s a cat
    return formatter
    }()
    var body: some View {
    VStack {
    Text(cat.name)
    Text(cat.breed)
    Text("\(cat.height, formatter: CatView.lengthFormatter)")
    }
    }
    }
    Jeff Kelley |

    View Slide

  68. SwiftUI and Errors
    Placeholder Values
    extension Cat {
    static var placeholder: Cat {
    return Cat(name: "Unknown")
    }
    }
    let publisher = catPublisher
    .catch { _ in Just(.placeholder) }
    Jeff Kelley |

    View Slide

  69. SwiftUI and Errors
    Showing Alerts
    struct CatFactView: View {
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    @State var fact: String?
    var body: some View {
    VStack {
    Text("Cat Fact of the Day")
    .font(.title)
    fact.map { Text($0) }
    }
    .onReceive(URLSession.shared.dataTaskPublisher(for: url)) { result in
    }
    }
    }
    Jeff Kelley |

    View Slide

  70. SwiftUI and Errors
    Showing Alerts
    struct CatFactView: View {
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    @State var fact: String?
    var body: some View {
    VStack {
    Text("Cat Fact of the Day")
    .font(.title)
    fact.map { Text($0) }
    }
    .onReceive(URLSession.shared.dataTaskPublisher(for: url)) { result in
    //
    !
    error: instance method 'onReceive(_:perform:)' requires the
    // types 'URLSession.DataTaskPublisher.Failure' (aka
    // 'URLError') and 'Never' be equivalent
    }
    }
    }
    Jeff Kelley |

    View Slide

  71. SwiftUI and Errors
    Showing Alerts
    struct CatFactView: View {
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    @State var fact: String?
    @State var networkError: URLError?
    @State var isPresentingError = false
    var factPublisher: AnyPublisher {
    return AnyPublisher(
    URLSession.shared.dataTaskPublisher(for: url)
    .map { String(data: $0.0, encoding: .utf8) }
    .catch { (error: URLError) -> Just in
    self.networkError = error
    self.isPresentingError = true
    return Just(nil)
    }
    )
    }
    var body: some View {
    VStack {
    Text("Cat Fact of the Day")
    .font(.title)
    fact.map { Text($0) }
    }
    .onReceive(factPublisher) { fact in
    self.fact = fact
    }
    .alert(isPresented: $isPresentingError) {
    Alert(title: Text("An error occurred loading this content."),
    message: self.networkError.map {
    Text($0.localizedDescription)
    })
    }
    }
    }
    Jeff Kelley |

    View Slide

  72. SwiftUI and Errors
    Showing Alerts
    let url = URL(string: "https://catfactsapi.cool/api/catfactoftheday")!
    var factPublisher: AnyPublisher {
    return AnyPublisher(
    URLSession.shared.dataTaskPublisher(for: url)
    .map { String(data: $0.0, encoding: .utf8) }
    .catch { (error: URLError) -> Just in
    self.networkError = error
    self.isPresentingError = true
    return Just(nil)
    }
    )
    }
    Jeff Kelley |

    View Slide

  73. SwiftUI and Errors
    Showing Alerts
    @State var fact: String?
    @State var networkError: URLError?
    @State var isPresentingError = false
    var body: some View {
    VStack {
    Text("Cat Fact of the Day")
    .font(.title)
    fact.map { Text($0) }
    }
    .onReceive(factPublisher) { fact in
    self.fact = fact
    }
    .alert(isPresented: $isPresentingError) {
    Alert(title: Text("An error occurred loading this content."),
    message: self.networkError.map {
    Text($0.localizedDescription)
    })
    }
    }
    Jeff Kelley |

    View Slide

  74. Agenda
    The History of Error-Handling: Objective-C and Swift
    The Swift Error Type
    Testing Errors
    Error Handling Through Types: Combine and SwiftUI
    Jeff Kelley |

    View Slide

  75. Questions?
    Contact info:
    Jeff Kelley
    @SlaunchaMan
    [email protected]

    View Slide