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

try! Swift 2017 – Error handling made easy

try! Swift 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

September 06, 2017
Tweet

Transcript

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

    Kostas Kremizas iOS Developer @Workable @kremizask
  2. Error = “a value used to report that an error

    condition occurred and normal functionality was skipped” Definitions – Matt Gallagher, Cocoa With Love
  3. 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
  4. – Swift Apprentice, Chapter 22 (Error Handling) “Error handling is

    the art of failing gracefully”
  5. Why bother? More and more critical actions in apps

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

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

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

    Right?
  10. None
  11. Steps 1. List possible errors For each method that can

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

    each method that can error out:
  13. Technical • Network • Disk • Server unavailability 1. List

    possible errors • Authorization • Validation • Stale or bad data Business
  14. 2. Handle each error Present a relevant error message

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

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

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

    is no difference between theory and practice. 
 In practice there is.”
  18. Adding error handling to a simple login screen

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

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

  21. @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")
  22. None
  23. @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.")
  24. None
  25. @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()
  26. None
  27. @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")
  28. @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") Logger.log(error) 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")
  29. Add error handling to a Compose email screen

  30. 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
  31. Add error handling to a Compose email screen

  32. @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()
  33. None
  34. @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() case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.")
  35. None
  36. @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() case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") default: showError(message: "Sorry, there was a problem. Please try again")
  37. None
  38. @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() case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") default: showError(message: "Sorry, there was a problem. Please try again") showMessage("Don't worry, a draft has been saved.")
  39. @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() case let error as NSError where error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet: showWarning(“You appear to be offline. Please check your connection.") default: showError(message: "Sorry, there was a problem. Please try again") showMessage("Don't worry, a draft has been saved.") Logger.log(error)
  40. None
  41. None
  42. None
  43. None
  44. None
  45. We end up with.. Boilerplate Repetition No standard way of

    handling Cognitive overhead
  46. Bad practices Skip cases and use generic error messages

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

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

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

  50. Make error handling as easy as ordering a burger

  51. lettuce bun tomato bacon cheddar onion patty

  52. “Can I have a burger with bun, bacon, cheddar, tomato,

    lettuce, onion and patty please?” - noone ever
  53. Instead we say… “The usual…” or “Cowboy burger with extra

    bacon without the onions”
  54. A set of default actions for common errors What we

    actually want
  55. 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
  56. ErrorHandler A library that provides a declarative fluent API for

    flexible error handling
  57. 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
  58. 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
  59. ErrorHandler() .on(error1, do: actionA) .on(error2, do: actionB) .onNoMatch(do: actionC) .always(do:

    log) .handle(error)
  60. Our strategy Setup a default ErrorHandler once

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

    ErrorHandler when needed based on the context
  62. In many cases all we will need is… ErrorHandler.defaultHandler.handle(error) if

    let error = error { return }
  63. None
  64. Back to our example

  65. .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 })
  66. None
  67. @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)
  68. None
  69. 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 { } } }
  70. Avoid the boilerplate Eliminate cognitive overhead Avoid the repetition Have

    a standard way of handling We get to..
  71. None
  72. Additional features Error Matchers

  73. 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)
  74. ErrorMatcher ErrorMatcher && || let notConnectedMatcher = NSErrorMatcher(domain: NSURLErrorDomain, code:

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

  76. // 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
  77. Additional features Error Matchers Tags Extension for http status error

    handling 
 (Alamofire out of the box)
  78. 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
  79. Thanks! 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