How to Handle Errors When You're Not Interested in the Details

I read a post by Erica Sadun about making try? more useful. This new variant of the keyword transforms thrown errors into nil. That can be convenient, but it doesn’t help handle the errors. Erica proposes a wrapper which logs errors. I like that idea and take it a step further.

Erica’s functions are pretty awesome as is. Have a look at how they’re used:

// Becomes nil on error
let contents = attempt { try NSFileManager.defaultManager()
    .contentsOfDirectoryAtPath(fakePath) }
let maybeWorked = attempt(potentiallyThrowingMethod())

// Returns false on error
let success = attemptFailable { try "Test".writeToFile(fakePath, 
    atomically: true, encoding: NSUTF8StringEncoding) }

Erica prints errors when they happen but doesn’t do much else with them in the sample codes. I bet she does in the resulting apps, though. The function definitions look like this at the moment:

public func attempt<T>(
        source: String = __FUNCTION__, 
        file: String = __FILE__, 
        line: Int = __LINE__, 
        closure: () throws -> T
        ) -> Optional<T> {
    do {
        return try closure()
    } catch {
        let fileName = (file as NSString).lastPathComponent
        let report = "Error \(fileName):\(source):\(line):\n    \(error)"
        print(report)
        return nil
    }
}

public func attemptFailable(
        source: String = __FUNCTION__, 
        file: String = __FILE__, 
        line: Int = __LINE__, 
        closure: () throws -> Void
        ) -> Bool {
    do {
        try closure()
        return true
    } catch {
        let fileName = (file as NSString).lastPathComponent
        let report = "Error \(fileName):\(source):\(line):\n    \(error)"
        print(report)
        return false
    }
}

Now what if you dispatched application errors to the user?

Especially most Core Data errors are in fact programmer errors. When they occur, you’re in charge to stop them from happening. File read errors or network connection failures are different. Failure on these fronts should be handled well by the application, but there’s no need to report every lost network connection to the developer.

An enhancement of Erica’s methods for production apps would be:

  • replace print with some kind of logging
  • send a message that an application-level error occured; use these for reporting

I wrote about my global applicationError helper function and the ErrorHandling module (on GitHub) already. The applicationError function is great to present misbehavior of the app to the user so she can report problems.

My Swift module will take care of the last point. Leaves logging.

Now I don’t want to implement my own logging framework. But I think we can register custom loggers if they adhere to a common signature:

// from https://github.com/DivineDominion/ErrorHandling#convenience-methods
class ErrorHandler {
    typealias Logger = (format: String, args: CVarArgType...) -> Void

    static var log: Logger = NSLog
    
    let log: Logger
    
    init() {
        self.log = ErrorHandling.log
    }
    
    // ...
}

The Logger signature is compatible to CocoaLumberjack and similar, too. It’s easy to set up the ErrorHandling module for logging if I add that line.

// Convenience function
public func attempt<T>(
        source: String = __FUNCTION__, 
        file: String = __FILE__, 
        line: Int = __LINE__, 
        closure: () throws -> T
        ) -> Optional<T>{
    
    let errorHandler = ErrorHandler()
    errorHandler.attempt(source: source, file: file, line: line, closure: closure)
}

extension ErrorHandler {
    func attempt<T>(
            source: String = __FUNCTION__, 
            file: String = __FILE__,
            line: Int = __LINE__,
            closure: () throws -> T
            ) -> Optional<T>{
        do {
            return try closure()
        } catch {
            let error = ErrorHandler.errorWithMessage(message: nil, function: source, file: file, line: line)
            handle(error)
            
            return nil
        }
    }
}

Recall that the ErrorHandler from my module’s repository does not much more than logging, showing an alert, and creating NSErrors:

extension ErrorHandler {
    public func handle(error: NSError?) {

        if let error = error {
            logError(error)
            reportError(error)
        }
    }
    
    private func logError(error: NSError) {

        log("Error: \(error)")
    }

    private func reportError(error: NSError) {

        ErrorAlert(error: error).displayModal()
    }
    
    public static func errorWithMessage(message: String? = nil, function: String = __FUNCTION__, file: String = __FILE__, line: Int = __LINE__) -> NSError {

        var userInfo: [String: AnyObject] = [
            functionKey: function,
            fileKey: file,
            lineKey: line,
        ]

        if let message = message {
            userInfo[NSLocalizedDescriptionKey] = message
        }

        return NSError(domain: exceptionDomain, code: 0, userInfo: userInfo)
    }
}

The ErrorAlert takes care of displaying problems to the user. That’s where formatting the report should take place; the part in question from Erica’s original source:

let fileName = (file as NSString).lastPathComponent
let report = "Error \(fileName):\(source):\(line):\n    \(error)"