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.

Kostas Kremizas

October 12, 2017
Tweet

More Decks by Kostas Kremizas

Other Decks in Programming

Transcript

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

    condition occurred and normal functionality was skipped” Definitions – Matt Gallagher, Cocoa With Love
  2. 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
  3. Why bother? More and more critical actions in apps Apps

    often rely on unreliable sources Bad error handling => users don't TRUST our app
  4. Technical • Network • Disk • Server unavailability 1. List

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

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

    is no difference between theory and practice. 
 In practice there is.”
  7. @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")
  8. @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")
  9. @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.")
  10. @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.")
  11. @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()
  12. @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()
  13. @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")
  14. @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)
  15. Add error handling to a Compose email screen [email protected] Issue

    Dear Natalie, I would like to ask you about a recent purchase of your product. I've having issues with... Thanks, Bob
  16. Add error handling to a Compose email screen [email protected] Issue

    Dear Natalie, I would like to ask you about a recent purchase of your product. I've having issues with... Thanks, Bob
  17. @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()
  18. @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()
  19. 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()
  20. 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()
  21. 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")
  22. 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")
  23. 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.")
  24. 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)
  25. 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
  26. 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
  27. 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
  28. Bad practices Skip cases and use generic error messages Don’t

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

    abstract and leave duplicate code everywhere Handle errors at the network layer !
  30. “Can I have a pizza with dough, buffalo mozzarella, tomato,

    tomato sauce, basil, olives and ham please?” - noone ever
  31. 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
  32. ErrorHandler public func on(matches: Error -> Bool, do action: @escaping

    ErrorAction) -> ErrorHandler public func always(do action: @escaping ErrorAction) -> ErrorHandler Basic API
  33. 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
  34. 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
  35. 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
  36. Our strategy Setup a default ErrorHandler once Customize the default

    ErrorHandler when needed based on the context
  37. 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 })
  38. .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 })
  39. .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 })
  40. .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 })
  41. @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)
  42. 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 { } } }
  43. Avoid the repetition Avoid the boilerplate No standard way of

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

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

    of handling Avoid the boilerplate We get to..
  46. 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)
  47. ErrorMatcher ErrorMatcher && || let notConnectedMatcher = NSErrorMatcher(domain: NSURLErrorDomain, code:

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

    NSURLErrorNotConnectedToInternet) let connectionLostMatcher = NSErrorMatcher(domain: NSURLErrorDomain, code: NSURLErrorNetworkConnectionLost) let offlineMatcher = notConnectedMatcher || connectionLostMatcher
  49. // 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
  50. 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
  51. 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