Upgrade to Pro — share decks privately, control downloads, hide ads and more …

#pragma conference 2017 - Error handling made easy

#pragma conference 2017 - Error handling made easy

UX doesn't only come down to looks and speed. Error handling is quite as important, and in order to get it right it has to be easy and straightforward. However, for most, it is still a mundane task with painfully too many cases to consider. In this talk we propose a recipe for reducing this friction and for adding complex error handling with just a few lines of code.

Daa1224b414d71fd51d29bad2d58221a?s=128

Kostas Kremizas

October 12, 2017
Tweet

Transcript

  1. Eleni Papanikolopoulou iOS Developer @Workable @elenipapanikolo ERROR HANDLING MADE EASY

    Kostas Kremizas iOS Developer @Workable @kremizask
  2. Definitions

  3. Error = “a value used to report that an error

    condition occurred and normal functionality was skipped” Definitions – Matt Gallagher, Cocoa With Love
  4. Error Handling = “code that looks for errors and performs

    different actions based on the presence of those errors” Error = “a value used to report that an error condition occurred and normal functionality was skipped” Definitions – Matt Gallagher, Cocoa With Love
  5. – Swift Apprentice, Chapter 22 (Error Handling) “Error handling is

    the art of failing gracefully”
  6. Why bother?

  7. Why bother? More and more critical actions in apps

  8. Why bother? More and more critical actions in apps Apps

    often rely on unreliable sources
  9. Why bother? More and more critical actions in apps Apps

    often rely on unreliable sources Bad error handling => users don't TRUST our app
  10. None
  11. Why so cryptic? It’s just copywriting and easy to fix

  12. Why so cryptic? It’s just copywriting and easy to fix

    Right?
  13. None
  14. Steps

  15. Steps For each method that can error out:

  16. Steps 1. List possible errors For each method that can

    error out:
  17. Steps 1. List possible errors 2. Handle each error For

    each method that can error out:
  18. 1. List possible errors

  19. Technical • Network • Disk • Server unavailability 1. List

    possible errors
  20. Technical • Network • Disk • Server unavailability 1. List

    possible errors • Authorization • Validation • Stale or bad data Business
  21. 2. Handle each error

  22. 2. Handle each error Present a relevant error message

  23. 2. Handle each error Present a relevant error message Additional

    actions per case
  24. 2. Handle each error Present a relevant error message Additional

    actions per case Log & report the error (always)
  25. – Jan L. A. van de Snepscheut “In theory there

    is no difference between theory and practice. 
 In practice there is.”
  26. Email client app Unlucky Bob

  27. Adding error handling to a simple login screen

  28. Adding error handling to a simple login screen unlucky.bob@gmail.com **********

  29. Adding error handling to a simple login screen unlucky.bob@gmail.com **********

  30. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } }
  31. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showAlert(message: "Incorrect email or password")
  32. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showAlert(message: "Incorrect email or password")
  33. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showAlert(message: "Incorrect email or password") case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.")
  34. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showAlert(message: "Incorrect email or password") case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.")
  35. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showAlert(message: "Incorrect email or password") case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") case let error as SignInError where error == .invalidEmail: showInvalidEmail()
  36. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showAlert(message: "Incorrect email or password") case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") case let error as SignInError where error == .invalidEmail: showInvalidEmail()
  37. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showAlert(message: "Incorrect email or password") case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") case let error as SignInError where error == .invalidEmail: showInvalidEmail() default: showError(message: "Sorry, there was a problem. Please try again")
  38. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showAlert(message: "Incorrect email or password") case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") case let error as SignInError where error == .invalidEmail: showInvalidEmail() default: showError(message: "Sorry, there was a problem. Please try again") Logger.log(error)
  39. Add error handling to a Compose email screen

  40. Add error handling to a Compose email screen natalie.sung@gmail.com Issue

    Dear Natalie, I would like to ask you about a recent purchase of your product. I've having issues with... Thanks, Bob
  41. Add error handling to a Compose email screen natalie.sung@gmail.com Issue

    Dear Natalie, I would like to ask you about a recent purchase of your product. I've having issues with... Thanks, Bob
  42. @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error)

    in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen()
  43. @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error)

    in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen()
  44. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen()
  45. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen()
  46. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen() default: showError(message: "Sorry, there was a problem. Please try again")
  47. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen() default: showError(message: "Sorry, there was a problem. Please try again")
  48. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen() default: showError(message: "Sorry, there was a problem. Please try again") showMessage("Don't worry, a draft has been saved.")
  49. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen() default: showError(message: "Sorry, there was a problem. Please try again") showMessage("Don't worry, a draft has been saved.") Logger.log(error)
  50. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen() default: showError(message: "Sorry, there was a problem. Please try again") showMessage("Don't worry, a draft has been saved.") Logger.log(error) common
  51. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen() default: showError(message: "Sorry, there was a problem. Please try again") showMessage("Don't worry, a draft has been saved.") Logger.log(error) common with a twist
  52. case let error as NSError where error.domain == NSURLErrorDomain &&

    error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in } } if let error = error { } switch error { } case let error as HttpError where error.status == 401: showSignInScreen() default: showError(message: "Sorry, there was a problem. Please try again") showMessage("Don't worry, a draft has been saved.") Logger.log(error) common with a twist NEW
  53. None
  54. None
  55. None
  56. None
  57. None
  58. We end up with…

  59. We end up with… Boilerplate

  60. We end up with… Boilerplate Repetition

  61. We end up with… Boilerplate Repetition No standard way of

    handling
  62. We end up with… Boilerplate Repetition No standard way of

    handling Cognitive overhead
  63. Bad practices

  64. Bad practices Skip cases and use generic error messages

  65. Bad practices Skip cases and use generic error messages Don’t

    abstract and leave duplicate code everywhere
  66. Bad practices Skip cases and use generic error messages Don’t

    abstract and leave duplicate code everywhere Handle errors at the network layer !
  67. What would 
 we want ideally?

  68. None
  69. Make error handling as easy as ordering a pizza

  70. tomato sauce tomato basil dough ham buffalo mozzarella olives

  71. “Can I have a pizza with dough, buffalo mozzarella, tomato,

    tomato sauce, basil, olives and ham please?” - noone ever
  72. Instead we say… “The usual…” or “Capricciosa pizza with extra

    olives without the mushrooms”
  73. What we actually want

  74. A set of default actions for common errors What we

    actually want
  75. A set of default actions for common errors An easy

    way to customise these defaults Add new cases Override existing ones Add actions for unknown errors Add actions for all errors (logging) What we actually want
  76. ErrorHandler A library that provides a declarative fluent API for

    flexible error handling
  77. ErrorHandler Basic API

  78. ErrorHandler public func on(matches: Error -> Bool, do action: @escaping

    ErrorAction) -> ErrorHandler Basic API
  79. ErrorHandler public func on(matches: Error -> Bool, do action: @escaping

    ErrorAction) -> ErrorHandler public func always(do action: @escaping ErrorAction) -> ErrorHandler Basic API
  80. ErrorHandler public func on(matches: Error -> Bool, do action: @escaping

    ErrorAction) -> ErrorHandler public func always(do action: @escaping ErrorAction) -> ErrorHandler public func onNoMatch(do action: @escaping ErrorAction) -> ErrorHandler Basic API
  81. ErrorHandler public func on(matches: Error -> Bool, do action: @escaping

    ErrorAction) -> ErrorHandler public func handle(_ error: Error) public func always(do action: @escaping ErrorAction) -> ErrorHandler public func onNoMatch(do action: @escaping ErrorAction) -> ErrorHandler Basic API
  82. public func on(matches: Error -> Bool, do action: @escaping ErrorAction)

    -> ErrorHandler public func handle(_ error: Error) public func always(do action: @escaping ErrorAction) -> ErrorHandler public func on(error: Error & Equatable, do action: @escaping ErrorAction) -> ErrorHandler Basic API public func onNoMatch(do action: @escaping ErrorAction) -> ErrorHandler
  83. ErrorHandler() .on(error1, do: actionA) .on(error2, do: actionB) .onNoMatch(do: actionC) .always(do:

    log) .handle(error)
  84. Our strategy

  85. Our strategy Setup a default ErrorHandler once

  86. Our strategy Setup a default ErrorHandler once Customize the default

    ErrorHandler when needed based on the context
  87. In many cases all we will need is… if let

    error = error { return }
  88. In many cases all we will need is… ErrorHandler.defaultHandler.handle(error) if

    let error = error { return }
  89. None
  90. Back to our example

  91. extension ErrorHandler { class var defaultHandler: ErrorHandler { } }

  92. extension ErrorHandler { class var defaultHandler: ErrorHandler { } }

  93. extension ErrorHandler { class var defaultHandler: ErrorHandler { } }

    return ErrorHandler()
  94. extension ErrorHandler { class var defaultHandler: ErrorHandler { } }

    return ErrorHandler() .on(NSError(domain:NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet), do: { (error) -> MatchingPolicy in showWarning(“You appear to be offline. Please check your connection.") return .continueMatching })
  95. .on(httpStatus: 401, do: { (error) -> MatchingPolicy in showSignInScreen() return

    .continueMatching }) extension ErrorHandler { class var defaultHandler: ErrorHandler { } } return ErrorHandler() .on(NSError(domain:NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet), do: { (error) -> MatchingPolicy in showWarning(“You appear to be offline. Please check your connection.") return .continueMatching })
  96. .always(do: { (error) -> MatchingPolicy in Logger.log(error) return .continueMatching })

    .on(httpStatus: 401, do: { (error) -> MatchingPolicy in showSignInScreen() return .continueMatching }) extension ErrorHandler { class var defaultHandler: ErrorHandler { } } return ErrorHandler() .on(NSError(domain:NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet), do: { (error) -> MatchingPolicy in showWarning(“You appear to be offline. Please check your connection.") return .continueMatching })
  97. .always(do: { (error) -> MatchingPolicy in Logger.log(error) return .continueMatching })

    .on(httpStatus: 401, do: { (error) -> MatchingPolicy in showSignInScreen() return .continueMatching }) extension ErrorHandler { class var defaultHandler: ErrorHandler { } } return ErrorHandler() .on(NSError(domain:NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet), do: { (error) -> MatchingPolicy in showWarning(“You appear to be offline. Please check your connection.") return .continueMatching }) .onNoMatch(do: { (error) -> MatchingPolicy in showError(message: "Sorry, there was a problem. Please try again") return .continueMatching })
  98. None
  99. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in if let error = error { } } }
  100. @IBAction func loginTapped(_ sender: Any) { api.login(email, password) { (response,

    error) in if let error = error { } } } ErrorHandler.defaultHandler .on(httpStatus: 401, do: { (_) -> MatchingPolicy in showAlert(message: "Incorrect email or password") return .stopMatching }) .on(SignInError.invalidEmail, do: { [weak self] (_) -> MatchingPolicy in self?.showInvalidEmail() return .stopMatching }) .handle(error)
  101. None
  102. @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error)

    in if let error = error { } } }
  103. ErrorHandler.defaultHandler .always(do: { (error) -> MatchingPolicy in self.showMessage("Don't worry, a

    draft has been saved.") return .continueMatching }) .handle(error) @IBAction func sendTapped(_ sender: Any) { api.send(emailData) { (response, error) in if let error = error { } } }
  104. Boilerplate Repetition No standard way of handling Cognitive overhead We

    get to..
  105. Avoid the boilerplate Repetition No standard way of handling Cognitive

    overhead We get to..
  106. Avoid the repetition Avoid the boilerplate No standard way of

    handling Cognitive overhead We get to..
  107. Avoid the repetition Have a standard way of handling Avoid

    the boilerplate Cognitive overhead We get to..
  108. Eliminate cognitive overhead Avoid the repetition Have a standard way

    of handling Avoid the boilerplate We get to..
  109. None
  110. Additional features

  111. Additional features Error Matchers

  112. public protocol ErrorMatcher { func matches(_ error: Error) -> Bool

    } ErrorMatcher handler .on(matcher) { (error) -> MatchingPolicy in showAlert(message: "It's a match!") return .stopMatching }.handle(error)
  113. ErrorMatcher ErrorMatcher && ||

  114. ErrorMatcher ErrorMatcher && || let notConnectedMatcher = NSErrorMatcher(domain: NSURLErrorDomain, code:

    NSURLErrorNotConnectedToInternet) let connectionLostMatcher = NSErrorMatcher(domain: NSURLErrorDomain, code: NSURLErrorNetworkConnectionLost)
  115. ErrorMatcher ErrorMatcher && || let notConnectedMatcher = NSErrorMatcher(domain: NSURLErrorDomain, code:

    NSURLErrorNotConnectedToInternet) let connectionLostMatcher = NSErrorMatcher(domain: NSURLErrorDomain, code: NSURLErrorNetworkConnectionLost) let offlineMatcher = notConnectedMatcher || connectionLostMatcher
  116. Additional features Error Matchers Tags

  117. // At the setup point of our default handler
 return

    ErrorHandler() // other setup code .tag(notConnectedMatcher, with: "offline") .tag(connectionLostMatcher, with: "offline") // At the point where we handle an error e.g. any controller ErrorHandler() .on(tag: "offline", do: { (error) -> MatchingPolicy in showError("You appear to be offline. Please check you connection.) return .continueMatching }).handle(error) Tag
  118. Additional features Error Matchers Tags Extension for http status error

    handling 
 (Alamofire out of the box)
  119. Conclusions Error handling is an integral part of a good

    UX It can get cumbersome if you want to do it right You can minimize the friction with the right abstractions
  120. DESIGN FOR ERRORS

  121. Questions? Even if there are many error cases, it’s nothing

    you can’t .handle() https://github.com/Workable/swift-error-handler.git https://github.com/Workable/java-error-handler.git