The use of NSError for Result

421 views
Skip to first unread message

Brad Larson

unread,
Sep 28, 2014, 4:22:01 PM9/28/14
to llam...@googlegroups.com
Rob and I had a conversation about this on Twitter, which requires more detail than that format allows, but I'm wondering if NSError will be the right long-term solution for an error type for Result.

A consensus seems to be forming around the use of an Either type for functions that return values, but that can also return errors. This avoids the need for magic constants, boolean return values, checking for nil, or the various other ways of signaling errors. It also avoids dealing with NSError double pointers as arguments, which make for ugly code and open you up to things like null pointer dereferences. Functions either return a value or an error, both contained within an Either variant. That forces you to consider error states (more robust code) and provides a consistent interface for returning errors (cleaner code).

The question now is about the error type itself. NSError has been the go-to error class within the Cocoa frameworks for a good long time, and we use it liberally within the code at our company. However, it was designed in a very different environment than we find ourselves in today with Swift, even predating concepts like Objective-C blocks. I'm beginning to question whether it is the right error type to use going forward. I fully recognize that Cocoa APIs (file handling, networking, etc.) will continue to return NSError, and will not change overnight, but I'm taking a good, hard look at the use of errors within our own Swift code. 

What does NSError provide us? It gives us a means of storing localized error descriptions, recovery options, and other description strings. It gives us an integer code, a string domain, and a user info dictionary. It lets us set a delegate for attempting recovery. That's quite a bit, bundled into one class.

However, in practice I find myself having some issues with elements of this design when using NSError for my own internal errors and error handling methods. I have to define my own integer enum lookup table for my own classes of errors. I find myself creating factory methods for generating NSErrors based on these enums. The use of error recovery delegates moves recovery code well away from where errors are defined, created, or handled.

As an alternative, I've been experimenting with Swift enums with associated values as an error type. This defines error codes as enumerated values, no mapping to integer codes required. This allows for associated user info in clearly defined types as needed by an error, not open-ended AnyObject dictionaries. Error descriptions can be provided using associate functions. Recovery could be handled using closures or at the point of error handling.

For example, our company builds robotic systems for printing microelectronics. Our control software is entirely Objective-C and Cocoa-based at present, and makes heavy use of NSError for various error states. We communicate with and coordinate the action of motors, control electronics, and CCD cameras, and each of those can have errors of varying severity. These aren't errors returned by Cocoa APIs, so we generate and consume our own NSErrors. After years of maintaining this code, we're investigating a serious redesign based on Swift and more functional principles. In the process of doing so, I tested using Swift enums for error states and liked what I saw.

Let's say we want to send a command and get a response from our control electronics, while accounting for a potential communications error. I can start with a generic Response type (an Either by a different name, for clarity of cases): 

enum Response<T, U> {
   
case Result (@autoclosure () -> T)
   
case Error (@autoclosure () -> U)
}



Let's give an error interface to simulate the kind of description you get from NSError:

protocol ErrorInterface {
   
var errorTitle: String { get }
   
var errorInfo: String { get }
}


and define our enum error type: 

enum CommunicationsError: ErrorInterface {
   
case ReadError
   
case WriteError
   
   
var errorTitle: String {
       
switch (self) {
           
case .ReadError: return "A read error occurred when communicating with the electronics."
           
case .WriteError: return "A write error occurred when communicating with the electronics."
       
}
   
}


   
var errorInfo: String {
       
switch (self) {
           
case .ReadError: return "Check to make sure that the control electronics box is connected to the computer.  If on, turn it off and on again.  When ready, click OK to retry the connection."
           
case .WriteError: return "Check to make sure that the control electronics box is connected to the computer.  If on, turn it off and on again.  When ready, click OK to retry the connection."
       
}
   
}
}



Here's a little logging function for the error type:

func presentError<T:ErrorInterface>(error:T) {
   
// Nominally, using NSApp -presentError
    println
("Error: Title: \(error.errorTitle) Info: \(error.errorInfo)")
}


Let's define some mock electronics communications functions:

func writeCommandToElectronics(command:String) -> Response<Int, CommunicationsError> {
   
return .Result(1)

}


func readBytesFromElectronics
(bytesToRead:Int) -> Response<[UInt8], CommunicationsError> {
   
// Create an artificial read error to test error response
   
if (bytesToRead == 7) {
       
return .Error(.ReadError)
   
}
   
else {
       
return .Result([UInt8](count:bytesToRead, repeatedValue:1))
   
}
}


where the write always succeeds, but we can trigger a read error using just the right input. These are then used in a combined function that writes a specific command to the electronics and reads the response:

func readValueAtFrequency(frequency:CGFloat) -> Response<CGFloat, CommunicationsError> {
   
switch (writeCommandToElectronics("F:\(frequency)")) {
       
case let .Error(error): return .Error(error)
       
case let .Result(bytesWritten): break
   
}


    let bytesToRead
= frequency > 200.0 ? 8 : 7
   
   
switch (readBytesFromElectronics(bytesToRead)) {
       
case let .Error(error): return .Error(error)
       
case let .Result(bytesRead):
            let calculatedValue
:CGFloat = CGFloat(bytesRead()[0]) / CGFloat(bytesRead()[1])
           
return .Result(calculatedImpedance)
   
}
}


At frequencies below 200.0, this triggers an artificial read error. Now, since we don't care to do recovery within this, maybe we could use a style of a bind operator to simplify this:

func readValueAtFrequency(frequency:CGFloat) -> Response<CGFloat, CommunicationsError> {
   
return writeCommandToElectronics("F:\(frequency)") >>== { bytesWritten in
        let bytesToRead
= frequency > 200.0 ? 8 : 7
       
return readBytesFromElectronics(bytesToRead) >>== { bytesRead in
            let calculatedValue
:CGFloat = CGFloat(bytesRead[0]) / CGFloat(bytesRead[1])
           
return .Result(calculatedImpedance)
       
}
   
}
}


Error recovery is handled at the point where readValueAtFrequency() is called, retrying the whole write / read operation if necessary to try for a soft recovery before taking more serious action or bubbling an error up to present to the user.

Now what I like about this is that I avoid needing a lookup table for error codes and factory methods for my own errors, I force myself to think about all error states at the point of recovery, and the class of error that a function can return is explicitly listed in its return type instead of a generic error. If I want to, I can create error enums that have other associated data with them, or that contain recovery closures which I define at the site of the error. It's also a value type vs. NSError's reference type.

For functions that require some kind of interface be present in your error types, protocols can be set up that the errors must comply to (localized descriptions for error dialogs, for example). You might even be able to craft an interface that NSError would comply to, along with these enum error types, for working with both in your code.

This is just brainstorming, but is something that I've been thinking about and something that came up while looking at the use of NSError as an error type for your Result.

Brad Larson

unread,
Sep 28, 2014, 4:25:04 PM9/28/14
to llam...@googlegroups.com
The last part got truncated, but here it is:

Rob Napier

unread,
Sep 28, 2014, 5:37:03 PM9/28/14
to llam...@googlegroups.com
Thanks for the examples. Just an intro note:

enum Response<T, U> {
   
case Result (@autoclosure () -> T)
   
case Error (@autoclosure () -> U)
}

I initially considered using @autoclosure here rather than Box(). It's really nice because its so transparent and will come out easily when the Swift compiler is finally fixed. Unfortunately, it's very dangerous. If you put the result of a mutable computation into the result, it'll be recomputed every time you access it. In many cases that won't matter, but when it does matter, it's really surprising. That's why I'm not recommending @autoclosure for anything you store (unless it's really obvious that you mean it to be a closure).

To the meat of it. I think your point here is possibly the whole answer:


For functions that require some kind of interface be present in your error types, protocols can be set up that the errors must comply to (localized descriptions for error dialogs, for example). You might even be able to craft an interface that NSError would comply to, along with these enum error types, for working with both in your code.


I just played around with it, and this might work just fine. I did this:

extension NSError: Error {
 
public var text: String { return self.description }
}

public protocol Error {
 
var text: String {get}
}

And then I just switched all the "NSError" references to "Error" and things kind of worked fine, except for my implementation of ==, which requires that I be able to compare errors. NSError doesn't conform to Equatable (though maybe we could make it somehow). Forcing people to implement Equatable for their Error might be a pain; I'm not sure. Do enums get Equatable automatically?

But the basic idea seems plausible and pretty flexible. You'd possibly wind up with code like this (in somewhat the worst case, but flexible enough to handle this case):

struct MyError: Error {
  let text
: String
  let code
: Int
}

    let err
= MyError(text:"stuff", code: 1)
    let nserr
= NSError(domain: "", code: 0, userInfo: nil)
   
    let x
: Result<Int> = Result.Failure(err)
    let y
: Result<Int> = Result.Failure(nserr)
   
   
if let returnedError = x.error() {
     
switch returnedError {
     
case let myError as MyError:
        let code
= myError.code
        println
("MyError: \(code)")

     
case is NSError:
        println
("NSError")

     
default:
        println
("Unknown error")
     
}
   
}


I'm not saying the above code is beautiful, but it shows the whole thing can kind of work, without even building any helpers.

Thoughts?
-Rob

Brad Larson

unread,
Sep 28, 2014, 6:14:06 PM9/28/14
to llam...@googlegroups.com
Yeah, the use of autoclosure was a quick hack for a minimal test case. Boxing does sound like the more reliable way to go in the meantime, until Apple gets the compiler working right with the generic enums. For the immutable values I was looking to use, I'd hoped the tricky cases wouldn't catch up to me in my prototyping, and this was simpler to rig up in a small example.

An error protocol might be one way to go, but what would be desired or needed for such a protocol? Do errors or Results need to be equatable? I don't know, which is a reason why I'm writing all this down.

Rob Napier

unread,
Sep 28, 2014, 6:43:20 PM9/28/14
to llam...@googlegroups.com
On Sun, Sep 28, 2014 at 6:14 PM, Brad Larson <lar...@sunsetlakesoftware.com> wrote:
An error protocol might be one way to go, but what would be desired or needed for such a protocol?

I'm not sure if we need anything in the protocol. It's legal for protocols to be empty, and just associate something with a type, and I think that's still much better than making it Any, while still giving the benefit of allowing multiple kinds of errors to flow through a chain.

But I'm open to anyone's suggestions on if there are any really obvious methods that should be provided by all error types.

 
Do errors or Results need to be equatable? I don't know, which is a reason why I'm writing all this down.

The more I've thought about this, the more I think they shouldn't be Equatable. It's not obvious what "equals" means for two failures with different errors. That's kind of equal (in that they're both "not a successful value") but kind of not. That suggests I should get rid of ==. I really created it for unit tests anyway. It's never come up in my live code.

-Rob
Reply all
Reply to author
Forward
0 new messages