barndog
barndog

Reputation: 7173

Swift - Error propagation while maintaining api clarity

With the addition of ErrorType to Swift, it's now possible to express error and failure events in a cleaner, more concise manner. We're no longer bound as iOS developers to the old way of NSError, clunky and hard to use.

ErrorType is great for a few reasons:

There are however some problems and one problem in particular that I've been running into lately and I'm curious to see how others solve this.

Say for example, you're build a social networking application akin to Facebook and you have Group model. When the user first loads your application you want to do two/three things:

  1. Fetch the relevant groups from your server.
  2. Fetch the already persisted groups on disk (Realm, CoreData, etc)
  3. Update the local copy with the remote copy just fetched.

Throughout this process, you can break the types of errors up into two distinct categories: PersistenceError and NetworkError where the ErrorType conforming enums might look like

enum PersistenceError: ErrorType {
    case CreateFailed([String: AnyObject)
    case FetchFailed(NSPredicate?)
}

enum NetworkError: ErrorType {
    case GetFailed(AnyObject.Type) // where AnyObject is your Group model class
}

There are several ways/design patterns you can use for delivering errors. The most common of course is try/catch.

func someFunc() throws {
    throw .GetFailed(Group.self)
}

Here, because functions that throw can't yet specify what type of error they're throwing, although I suspect that will change, you can easily throw a NetworkError or a PersistenceError.

The trouble comes in when using a more generic or functional approach, such as ReactiveCocoa or Result.

func fetchGroupsFromRemote() -> SignalProducer<[Group], NetworkError> {
   // fetching code here
}

func fetchGroupsFromLocal() -> SignalProducer<[Group], PersistenceError> {
    // fetch from local
}

Then wrapping the two calls:

func fetch() -> SignalProducer<Group, ???> {
   let remoteProducer = self.fetchGroupsFromRemote()
   .flatMap(.Concat) ) { self.saveGroupsToLocal($0) // returns SignalProducer<[Group], PersistenceError> }
   let localProducer = self.fetchGroupsFromLocal()
   return SignalProducer(values: [localProducer, remoteProducer]).flatten(.Merge)
}

What error type goes in the spot marked ???? Is it NetworkError or PersistenceError? You can't use ErrorType because it can't be used as a concrete type and if you try using it as a generic constraint, <E: ErrorType>, the compiler will still complain saying it expects an argument list of type E.

So the issue becomes, less so with try/catch and more so with functional approaches, how to maintain an error hierarchy structure so that error information can be preserved throughout different ErrorType conformances while still having that descriptive error api.

The best I can come up with so far is:

enum Error: ErrorType {
    // Network Errors
    case .GetFailed

   // Persistence Errors
   case .FetchFailed

   // More error types
}

which is essentially one long error enum so that any and all errors are of the same type and even the deepest error can be propagated up the chain.

How do other people deal with this? I enjoy the benefits of having one universal error enum but the readability and api clarify suffer. I'd much rather have each function describe what specific error cluster they return rather than have each return Error but again, I don't see how to do that without losing error information along the way.

Upvotes: 1

Views: 153

Answers (1)

Tushar
Tushar

Reputation: 3052

Just trying a solution to your problem, may not be a perfect one. Using protocol to easily pass around the error objects :

//1.
protocol Error {
  func errorDescription()
}

//2.
enum PersistenceError: ErrorType, Error {
  case CreateFailed([String: AnyObject)
  case FetchFailed(NSPredicate?)

  func errorDescription() {

  }
}

//3.
enum NetworkError: ErrorType, Error {
  case GetFailed(AnyObject.Type) // where AnyObject is your Group model class
  func errorDescription() {

  }
}

//5.
func fetch() -> SignalProducer<Group, Error> {
  ...
}

Upvotes: 1

Related Questions