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

Custom Error Handling on iOS

Custom Error Handling on iOS

This is a talk about the approach we take at HRS to handle errors globally within our app by taking advantage of the responder chain and the very powerful but rarely known `NSErrorRecoveryAttempting` protocol. I talk about our implementation of this protocol, how we present errors, the path an error takes through our app, and how error handling can be much less painful than it usually is.

Michael Ochs

August 18, 2014
Tweet

More Decks by Michael Ochs

Other Decks in Programming

Transcript

  1. View Slide

  2. Custom error handling on iOS
    Michael Ochs, Mobile Developer, HRS

    View Slide

  3. Agenda
    • Responsibility
    • NSError / NSErrorRecoveryAttempting
    • Presentation
    • UIResponder
    • What’s next?

    View Slide

  4. Error handling before

    View Slide

  5. How not to do it

    View Slide

  6. How not to do it
    NSError *error = nil;

    BOOL success = [self.data writeToFile:@"my/path"

    options:NSDataWritingAtomic

    error:&error];

    if (!success) {

    // TODO: Implement error handling

    }


    View Slide

  7. How not to do it
    NSError *error = nil;

    BOOL success = [self.data writeToFile:@"my/path"

    options:NSDataWritingAtomic

    error:&error];

    if (!success) {

    // TODO: Implement error handling

    }


    View Slide

  8. How not to do it
    • Implemented in UIViewController subclasses
    • Custom in each controller
    • Various error messages for the same error
    • The same error message for various errors

    View Slide

  9. Responsibility

    View Slide

  10. Responsibility
    • Globally in the app
    • Usable by every potential error receiver
    • Easy to use
    • Little setup effort
    • Error recovery

    View Slide

  11. Responsibility
    !
    • what went wrong
    • does a retry make sense
    • how to recover
    !
    • what action led to the error
    • is a retry possible
    • how to retry
    Creator of the error knows:
    Consumer of the error knows:

    View Slide

  12. Responsibility
    Trigger action
    Create error
    Present error
    Recover from error
    Retry action

    View Slide

  13. Responsibility
    Trigger action
    Consumer Creator
    Create error
    Present error
    Recover from error
    Retry action

    View Slide

  14. Responsibility
    Trigger action
    Create error
    Present error
    Recover from error
    Retry action
    Controller Data
    Application

    View Slide

  15. NSError

    View Slide

  16. NSError
    • NSLocalizedDescriptionKey
    • NSLocalizedFailureReasonErrorKey
    • NSLocalizedRecoverySuggestionErrorKey
    • NSLocalizedRecoveryOptionsErrorKey
    • NSRecoveryAttempterErrorKey

    View Slide

  17. NSError
    The corresponding value is a localized string representation of the
    error that, if present, will be returned by localizedDescription.
    NSLocalizedDescriptionKey

    View Slide

  18. NSError
    The corresponding value is a localized string representation
    containing the reason for the failure that, if present, will be returned by
    localizedFailureReason.
    This string provides a more detailed explanation of the error than the
    description.
    NSLocalizedFailureReasonErrorKey

    View Slide

  19. NSError
    The corresponding value is a string containing the localized recovery
    suggestion for the error.
    This string is suitable for displaying as the secondary message in an
    alert panel.
    NSLocalizedRecoverySuggestionErrorKey

    View Slide

  20. NSError
    The corresponding value is an array containing the localized titles of
    buttons appropriate for displaying in an alert panel.
    The first string is the title of the right-most and default button, the
    second the one to the left, and so on. The recovery options should be
    appropriate for the recovery suggestion returned by
    localizedRecoverySuggestion.
    NSLocalizedRecoveryOptionsErrorKey

    View Slide

  21. NSError
    The corresponding value is an object that conforms to the
    NSErrorRecoveryAttempting informal protocol.
    The recovery attempter must be an object that can correctly interpret
    an index into the array returned by localizedRecoveryOptions.
    NSRecoveryAttempterErrorKey

    View Slide

  22. NSError
    • NSLocalizedDescriptionKey
    • NSLocalizedFailureReasonErrorKey
    • NSLocalizedRecoverySuggestionErrorKey
    • NSLocalizedRecoveryOptionsErrorKey
    • NSRecoveryAttempterErrorKey

    View Slide

  23. NSError
    - (BOOL)doSaveOperation:(NSError **)error {


    NSError *error = [NSError errorWithDomain:MyErrorDomain

    code:MyErrorCode

    userInfo:userInfo];

    *error = error;


    return NO;

    }
    Error creation

    View Slide

  24. NSError
    NSDictionary *userInfo = @{

    NSLocalizedFailureReasonErrorKey:

    @"Something went wrong",


    NSLocalizedRecoverySuggestionErrorKey:

    @"Not successful. Try again!",


    NSLocalizedRecoveryOptionsErrorKey:

    @[ @"Cancel", @"Retry" ],


    NSRecoveryAttempterErrorKey:

    recoveryAttempter

    };
    Error creation

    View Slide

  25. NSError
    NSDictionary *userInfo = @{

    NSLocalizedFailureReasonErrorKey:

    @"Something went wrong",


    NSLocalizedRecoverySuggestionErrorKey:

    @"Not successful. Try again!",


    NSLocalizedRecoveryOptionsErrorKey:

    [recoveryAttempter localizedRecoveryOptions],


    NSRecoveryAttempterErrorKey:

    recoveryAttempter

    };
    Error creation

    View Slide

  26. NSErrorRecoveryAttempting

    View Slide

  27. NSErrorRecoveryAttempting
    • Informal protocol on NSObject
    • Integrated with NSError
    • Handles error recovery

    View Slide

  28. NSErrorRecoveryAttempting
    - (void)attemptRecoveryFromError:(NSError *)error

    optionIndex:(NSUInteger)recoveryOptionIndex

    delegate:(id)delegate

    didRecoverSelector:(SEL)didRecoverSelector

    contextInfo:(void *)contextInfo;


    - (BOOL)attemptRecoveryFromError:(NSError *)error

    optionIndex:(NSUInteger)recoveryOptionIndex;

    View Slide

  29. NSErrorRecoveryAttempting
    - (void)attemptRecoveryFromError:(NSError *)error

    optionIndex:(NSUInteger)recoveryOptionIndex

    delegate:(id)delegate

    didRecoverSelector:(SEL)didRecoverSelector

    contextInfo:(void *)contextInfo;


    - (BOOL)attemptRecoveryFromError:(NSError *)error

    optionIndex:(NSUInteger)recoveryOptionIndex;

    View Slide

  30. NSErrorRecoveryAttempting
    - (void)attemptRecoveryFromError:(NSError *)error

    optionIndex:(NSUInteger)recoveryOptionIndex

    delegate:(id)delegate

    didRecoverSelector:(SEL)didRecoverSelector

    contextInfo:(void *)contextInfo;


    - (BOOL)attemptRecoveryFromError:(NSError *)error

    optionIndex:(NSUInteger)recoveryOptionIndex;

    View Slide

  31. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter

    View Slide

  32. NSErrorRecoveryAttempting
    - (void)addRecoveryOptionWithTitle:(NSString *)title

    recoveryAttempt:(BOOL(^)())recoveryBlock;


    - (NSArray *)localizedRecoveryOptions;
    HRSErrorRecoveryAttempter

    View Slide

  33. NSErrorRecoveryAttempting
    - (void)addRecoveryOptionWithTitle:(NSString *)title

    recoveryAttempt:(BOOL(^)())recoveryBlock;


    - (NSArray *)localizedRecoveryOptions;
    HRSErrorRecoveryAttempter

    View Slide

  34. NSErrorRecoveryAttempting
    - (void)addRecoveryOptionWithTitle:(NSString *)title

    recoveryAttempt:(BOOL(^)())recoveryBlock;


    - (NSArray *)localizedRecoveryOptions;
    HRSErrorRecoveryAttempter

    View Slide

  35. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter *recoveryAttempter =

    [HRSErrorRecoveryAttempter new];


    [recoveryAttempter addCancelRecoveryOption];


    [recoveryAttempter addRecoveryOptionWithTitle:@"Retry"

    recoveryAttempt:^{

    return YES;

    }];
    HRSErrorRecoveryAttempter

    View Slide

  36. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter *recoveryAttempter =

    [HRSErrorRecoveryAttempter new];


    [recoveryAttempter addCancelRecoveryOption];


    [recoveryAttempter addRecoveryOptionWithTitle:@"Retry"

    recoveryAttempt:^{

    return YES;

    }];
    HRSErrorRecoveryAttempter

    View Slide

  37. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter *recoveryAttempter =

    [HRSErrorRecoveryAttempter new];


    [recoveryAttempter addCancelRecoveryOption];


    [recoveryAttempter addRecoveryOptionWithTitle:@"Retry"

    recoveryAttempt:^{

    return YES;

    }];
    HRSErrorRecoveryAttempter

    View Slide

  38. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter *recoveryAttempter =

    [HRSErrorRecoveryAttempter new];


    [recoveryAttempter addCancelRecoveryOption];


    [recoveryAttempter addRecoveryOptionWithTitle:@"Retry"

    recoveryAttempt:^{

    return YES;

    }];
    HRSErrorRecoveryAttempter

    View Slide

  39. Presentation

    View Slide

  40. Presentation
    @interface NSResponder (NSErrorPresentation)


    - (BOOL)presentError:(NSError *)anError;


    - (void)presentError:(NSError *)error

    modalForWindow:(NSWindow *)aWindow

    delegate:(id)delegate

    didPresentSelector:(SEL)didPresentSelector

    contextInfo:(void *)contextInfo;


    - (NSError *)willPresentError:(NSError *)anError;


    @end
    OS X

    View Slide

  41. Presentation
    @interface NSResponder (NSErrorPresentation)


    - (BOOL)presentError:(NSError *)anError;


    - (void)presentError:(NSError *)error

    modalForWindow:(NSWindow *)aWindow

    delegate:(id)delegate

    didPresentSelector:(SEL)didPresentSelector

    contextInfo:(void *)contextInfo;


    - (NSError *)willPresentError:(NSError *)anError;


    @end
    OS X

    View Slide

  42. Presentation
    @interface NSResponder (NSErrorPresentation)


    - (BOOL)presentError:(NSError *)anError;


    - (void)presentError:(NSError *)error

    modalForWindow:(NSWindow *)aWindow

    delegate:(id)delegate

    didPresentSelector:(SEL)didPresentSelector

    contextInfo:(void *)contextInfo;


    - (NSError *)willPresentError:(NSError *)anError;


    @end
    OS X

    View Slide

  43. Presentation
    @interface NSResponder (NSErrorPresentation)


    - (BOOL)presentError:(NSError *)anError;


    - (void)presentError:(NSError *)error

    modalForWindow:(NSWindow *)aWindow

    delegate:(id)delegate

    didPresentSelector:(SEL)didPresentSelector

    contextInfo:(void *)contextInfo;


    - (NSError *)willPresentError:(NSError *)anError;


    @end
    OS X

    View Slide

  44. Presentation
    @interface UIResponder (HRSCustomErrorPresentation)


    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL didRecover))completionHandler;


    - (NSError *)willPresentError:(NSError *)anError;


    @end
    iOS

    View Slide

  45. Presentation
    @interface UIResponder (HRSCustomErrorPresentation)


    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL didRecover))completionHandler;


    - (NSError *)willPresentError:(NSError *)anError;


    @end
    iOS

    View Slide

  46. Presentation
    @interface UIResponder (HRSCustomErrorPresentation)


    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL didRecover))completionHandler;


    - (NSError *)willPresentError:(NSError *)anError;


    @end
    iOS

    View Slide

  47. Presentation
    - (IBAction)save:(id)sender {

    NSError *error = nil;

    BOOL success = [self.data writeToFile:@"my/path"

    options:NSDataWritingAtomic

    error:&error];

    if (!success) {

    [self presentError:error

    completionHandler:^(BOOL didRecover) {

    if (didRecover) {

    [self save:sender];

    }

    }];

    }

    }
    iOS

    View Slide

  48. Presentation
    - (IBAction)save:(id)sender {

    NSError *error = nil;

    BOOL success = [self.data writeToFile:@"my/path"

    options:NSDataWritingAtomic

    error:&error];

    if (!success) {

    [self presentError:error

    completionHandler:^(BOOL didRecover) {

    if (didRecover) {

    [self save:sender];

    }

    }];

    }

    }
    iOS

    View Slide

  49. Presentation
    - (IBAction)save:(id)sender {

    NSError *error = nil;

    BOOL success = [self.data writeToFile:@"my/path"

    options:NSDataWritingAtomic

    error:&error];

    if (!success) {

    [self presentError:error

    completionHandler:^(BOOL didRecover) {

    if (didRecover) {

    [self save:sender];

    }

    }];

    }

    }
    iOS

    View Slide

  50. Architecture

    View Slide

  51. Architecture
    - presentError:completionHandler:
    NSError

    - doSomething:
    UIAlertView

    View Slide

  52. Architecture
    - presentError:completionHandler:
    NSError

    - doSomething:
    UIAlertView

    completionHandler:

    void(^)(BOOL didRecover)
    HRSErrorRecoveryAttempter HRSErrorPresentationDelegate

    View Slide

  53. The path of an error

    View Slide

  54. NSError
    UIAlertView
    -attemptRecoveryFromError:

    optionIndex:
    didRecover
    void (^completionHandler)(BOOL didRecover)
    The path of an error
    UIViewController Operation
    AppDelegate
    -save: - doSomething:
    -presentError:completionHandler:

    View Slide

  55. UIResponder

    View Slide

  56. UIResponder
    - presentError:completionHandler:

    View Slide

  57. UIResponder
    Subclasses
    • UIView
    • UIViewController
    • UIApplication

    • SKNode

    View Slide

  58. UIApplication
    UIWindow
    UIResponder
    UIView

    - (UIResponder *)nextResponder;
    UIView
    UIView
    UIViewController
    UIView
    UIViewController

    View Slide

  59. UIResponder
    @interface DataSource : UIResponder 

    @property (nonatomic, weak) id delegate;

    @end


    @implementation DataSource


    - (UIResponder *)nextResponder {

    if ([self.delegate isKindOfClass:[UIResponder class]]) {

    return self.delegate;

    }

    return nil;

    }


    @end

    View Slide

  60. UIResponder
    @interface DataSource : UIResponder 

    @property (nonatomic, weak) id delegate;

    @end


    @implementation DataSource


    - (UIResponder *)nextResponder {

    if ([self.delegate isKindOfClass:[UIResponder class]]) {

    return self.delegate;

    }

    return nil;

    }


    @end

    View Slide

  61. UIResponder
    @interface DataSource : UIResponder 

    @property (nonatomic, weak) id delegate;

    @end


    @implementation DataSource


    - (UIResponder *)nextResponder {

    if ([self.delegate isKindOfClass:[UIResponder class]]) {

    return self.delegate;

    }

    return nil;

    }


    @end

    View Slide

  62. UIResponder
    UIResponder (HRSCustomErrorPresentation)

    View Slide

  63. UIResponder
    - (NSError *)willPresentError:(NSError *)anError {

    return anError;

    }
    UIResponder (HRSCustomErrorPresentation)

    View Slide

  64. UIResponder
    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL didRecover))completionHandler {


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }


    UIResponder *nextResponder = ([self nextResponder] ?:

    [UIApplication sharedApplication]);

    [nextResponder presentError:anError

    completionHandler:completionHandler];

    }
    UIResponder (HRSCustomErrorPresentation)

    View Slide

  65. UIResponder
    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL didRecover))completionHandler {


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }


    UIResponder *nextResponder = ([self nextResponder] ?:

    [UIApplication sharedApplication]);

    [nextResponder presentError:anError

    completionHandler:completionHandler];

    }
    UIResponder (HRSCustomErrorPresentation)

    View Slide

  66. UIResponder
    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL didRecover))completionHandler {


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }


    UIResponder *nextResponder = ([self nextResponder] ?:

    [UIApplication sharedApplication]);

    [nextResponder presentError:anError

    completionHandler:completionHandler];

    }
    UIResponder (HRSCustomErrorPresentation)

    View Slide

  67. UIResponder
    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL didRecover))completionHandler {


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }


    UIResponder *nextResponder = ([self nextResponder] ?:

    [UIApplication sharedApplication]);

    [nextResponder presentError:anError

    completionHandler:completionHandler];

    }
    UIResponder (HRSCustomErrorPresentation)

    View Slide

  68. UIResponder
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  69. UIResponder
    - (NSError *)willPresentError:(NSError *)anError {

    if (anError.recoveryAttempter == nil) {

    NSDictionary *userInfo = @{ … };

    anError = [NSError errorWithDomain:anError.domain

    code:anError.code

    userInfo:userInfo];

    }

    return anError;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  70. UIResponder
    - (NSError *)willPresentError:(NSError *)anError {

    if (anError.recoveryAttempter == nil) {

    NSDictionary *userInfo = @{ … };

    anError = [NSError errorWithDomain:anError.domain

    code:anError.code

    userInfo:userInfo];

    }

    return anError;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  71. UIResponder
    - (NSError *)willPresentError:(NSError *)anError {

    if (anError.recoveryAttempter == nil) {

    NSDictionary *userInfo = @{ … };

    anError = [NSError errorWithDomain:anError.domain

    code:anError.code

    userInfo:userInfo];

    }

    return anError;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  72. UIResponder
    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL))completionHandler {


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  73. UIResponder
    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL))completionHandler {


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  74. UIResponder
    - (void)presentError:(NSError *)anError

    completionHandler:(void (^)(BOOL))completionHandler {


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  75. UIResponder
    HRSErrorPresentationDelegate *delegate = …;


    UIAlertView *alertView = [[UIAlertView alloc]

    initWithTitle:[anError localizedFailureReason]

    message:[anError localizedRecoverySuggestion]

    delegate:delegate

    cancelButtonTitle:nil

    otherButtonTitles:nil];


    for (NSString *title in [anError localizedRecoveryOptions]) {

    [alertView addButtonWithTitle:title];

    }


    [alertView show];
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  76. UIResponder
    HRSErrorPresentationDelegate *delegate = …;


    UIAlertView *alertView = [[UIAlertView alloc]

    initWithTitle:[anError localizedFailureReason]

    message:[anError localizedRecoverySuggestion]

    delegate:delegate

    cancelButtonTitle:nil

    otherButtonTitles:nil];


    for (NSString *title in [anError localizedRecoveryOptions]) {

    [alertView addButtonWithTitle:title];

    }


    [alertView show];
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  77. UIResponder
    HRSErrorPresentationDelegate *delegate = …;


    UIAlertView *alertView = [[UIAlertView alloc]

    initWithTitle:[anError localizedFailureReason]

    message:[anError localizedRecoverySuggestion]

    delegate:delegate

    cancelButtonTitle:nil

    otherButtonTitles:nil];


    for (NSString *title in [anError localizedRecoveryOptions]) {

    [alertView addButtonWithTitle:title];

    }


    [alertView show];
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  78. UIResponder
    HRSErrorPresentationDelegate *delegate = …;


    UIAlertView *alertView = [[UIAlertView alloc]

    initWithTitle:[anError localizedFailureReason]

    message:[anError localizedRecoverySuggestion]

    delegate:delegate

    cancelButtonTitle:nil

    otherButtonTitles:nil];


    for (NSString *title in [anError localizedRecoveryOptions]) {

    [alertView addButtonWithTitle:title];

    }


    [alertView show];
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  79. UIResponder
    HRSErrorPresentationDelegate *delegate = …;


    UIAlertView *alertView = [[UIAlertView alloc]

    initWithTitle:[anError localizedFailureReason]

    message:[anError localizedRecoverySuggestion]

    delegate:delegate

    cancelButtonTitle:nil

    otherButtonTitles:nil];


    for (NSString *title in [anError localizedRecoveryOptions]) {

    [alertView addButtonWithTitle:title];

    }


    [alertView show];
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  80. UIResponder
    objc_setAssociatedObject(alertView,

    DelegateAssociation,

    delegate,

    OBJC_ASSOCIATION_RETAIN);


    }
    AppDelegate (HRSCustomErrorPresentation)

    View Slide

  81. What is next?

    View Slide

  82. What’s next?
    • Handle common errors
    • Provide custom error presentation UI
    • Add specific error recovery attempter
    • This is all loosely coupled

    View Slide

  83. What’s next?
    • Available open source
    • Currently considered beta
    • Internal interaction might still change
    • Feedback welcome

    View Slide

  84. Related topics
    • Realmac’s “Cocoa error handling and recovery”

    http://realmacsoftware.com/blog/cocoa-error-handling-and-recovery

    View Slide

  85. Feedback / Questions
    @_mochs
    [email protected]
    ios-coding.com

    View Slide

  86. Thank you

    View Slide