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. Custom error handling on iOS
    Michael Ochs, Mobile Developer, HRS

    View full-size slide

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

    View full-size slide

  3. Error handling before

    View full-size slide

  4. How not to do it

    View full-size slide

  5. 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 full-size 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 full-size slide

  7. 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 full-size slide

  8. Responsibility

    View full-size slide

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

    View full-size slide

  10. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

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

    View full-size slide

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


    NSError *error = [NSError errorWithDomain:MyErrorDomain

    code:MyErrorCode

    userInfo:userInfo];

    *error = error;


    return NO;

    }
    Error creation

    View full-size slide

  22. NSError
    NSDictionary *userInfo = @{

    NSLocalizedFailureReasonErrorKey:

    @"Something went wrong",


    NSLocalizedRecoverySuggestionErrorKey:

    @"Not successful. Try again!",


    NSLocalizedRecoveryOptionsErrorKey:

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


    NSRecoveryAttempterErrorKey:

    recoveryAttempter

    };
    Error creation

    View full-size slide

  23. NSError
    NSDictionary *userInfo = @{

    NSLocalizedFailureReasonErrorKey:

    @"Something went wrong",


    NSLocalizedRecoverySuggestionErrorKey:

    @"Not successful. Try again!",


    NSLocalizedRecoveryOptionsErrorKey:

    [recoveryAttempter localizedRecoveryOptions],


    NSRecoveryAttempterErrorKey:

    recoveryAttempter

    };
    Error creation

    View full-size slide

  24. NSErrorRecoveryAttempting

    View full-size slide

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

    View full-size slide

  26. 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 full-size slide

  27. 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 full-size 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 full-size slide

  29. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter

    View full-size slide

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

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


    - (NSArray *)localizedRecoveryOptions;
    HRSErrorRecoveryAttempter

    View full-size slide

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

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


    - (NSArray *)localizedRecoveryOptions;
    HRSErrorRecoveryAttempter

    View full-size slide

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

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


    - (NSArray *)localizedRecoveryOptions;
    HRSErrorRecoveryAttempter

    View full-size slide

  33. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter *recoveryAttempter =

    [HRSErrorRecoveryAttempter new];


    [recoveryAttempter addCancelRecoveryOption];


    [recoveryAttempter addRecoveryOptionWithTitle:@"Retry"

    recoveryAttempt:^{

    return YES;

    }];
    HRSErrorRecoveryAttempter

    View full-size slide

  34. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter *recoveryAttempter =

    [HRSErrorRecoveryAttempter new];


    [recoveryAttempter addCancelRecoveryOption];


    [recoveryAttempter addRecoveryOptionWithTitle:@"Retry"

    recoveryAttempt:^{

    return YES;

    }];
    HRSErrorRecoveryAttempter

    View full-size slide

  35. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter *recoveryAttempter =

    [HRSErrorRecoveryAttempter new];


    [recoveryAttempter addCancelRecoveryOption];


    [recoveryAttempter addRecoveryOptionWithTitle:@"Retry"

    recoveryAttempt:^{

    return YES;

    }];
    HRSErrorRecoveryAttempter

    View full-size slide

  36. NSErrorRecoveryAttempting
    HRSErrorRecoveryAttempter *recoveryAttempter =

    [HRSErrorRecoveryAttempter new];


    [recoveryAttempter addCancelRecoveryOption];


    [recoveryAttempter addRecoveryOptionWithTitle:@"Retry"

    recoveryAttempt:^{

    return YES;

    }];
    HRSErrorRecoveryAttempter

    View full-size slide

  37. Presentation

    View full-size slide

  38. 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 full-size slide

  39. 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 full-size 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 full-size 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 full-size slide

  42. Presentation
    @interface UIResponder (HRSCustomErrorPresentation)


    - (void)presentError:(NSError *)anError

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


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


    @end
    iOS

    View full-size slide

  43. Presentation
    @interface UIResponder (HRSCustomErrorPresentation)


    - (void)presentError:(NSError *)anError

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


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


    @end
    iOS

    View full-size slide

  44. Presentation
    @interface UIResponder (HRSCustomErrorPresentation)


    - (void)presentError:(NSError *)anError

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


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


    @end
    iOS

    View full-size slide

  45. 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 full-size slide

  46. 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 full-size 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 full-size slide

  48. Architecture

    View full-size slide

  49. Architecture
    - presentError:completionHandler:
    NSError

    - doSomething:
    UIAlertView

    View full-size slide

  50. Architecture
    - presentError:completionHandler:
    NSError

    - doSomething:
    UIAlertView

    completionHandler:

    void(^)(BOOL didRecover)
    HRSErrorRecoveryAttempter HRSErrorPresentationDelegate

    View full-size slide

  51. The path of an error

    View full-size slide

  52. NSError
    UIAlertView
    -attemptRecoveryFromError:

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

    View full-size slide

  53. UIResponder
    - presentError:completionHandler:

    View full-size slide

  54. UIResponder
    Subclasses
    • UIView
    • UIViewController
    • UIApplication

    • SKNode

    View full-size slide

  55. UIApplication
    UIWindow
    UIResponder
    UIView

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

    View full-size slide

  56. 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 full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

  59. UIResponder
    UIResponder (HRSCustomErrorPresentation)

    View full-size slide

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

    return anError;

    }
    UIResponder (HRSCustomErrorPresentation)

    View full-size slide

  61. 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 full-size slide

  62. 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 full-size slide

  63. 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 full-size 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 full-size slide

  65. UIResponder
    AppDelegate (HRSCustomErrorPresentation)

    View full-size slide

  66. 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 full-size slide

  67. 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 full-size slide

  68. 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 full-size slide

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

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


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View full-size slide

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

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


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View full-size slide

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

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


    anError = [self willPresentError:anError];

    if (anError == nil) {

    return;

    }
    AppDelegate (HRSCustomErrorPresentation)

    View full-size slide

  72. 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 full-size slide

  73. 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 full-size slide

  74. 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 full-size 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 full-size 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 full-size slide

  77. UIResponder
    objc_setAssociatedObject(alertView,

    DelegateAssociation,

    delegate,

    OBJC_ASSOCIATION_RETAIN);


    }
    AppDelegate (HRSCustomErrorPresentation)

    View full-size slide

  78. What is next?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

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

    View full-size slide

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

    View full-size slide