Josh Paradroid
Josh Paradroid

Reputation: 1414

How to handle with a multiple errors in a completion closure

I'm implementing a function, which takes an array and a completion closure. I iterate over the items in the array, performing a network request based on each item. When each request is finished, it appends its results to a collection.

Once all the requests have returned, the function's completion block is called with the accumulated collection.

My issue is that I'm not sure about how to handle the (potentially multiple) errors that the network request might provide. Is there an standard way to deal with this pattern? The first ideas that come to mind are:

Neither of these options seem ideal. Is there a standard way to deal with this issue? I'm not sure what a user of this function would reasonably expect.

I'm implementing this in Swift 3.1.

Upvotes: 0

Views: 611

Answers (1)

pbodsk
pbodsk

Reputation: 6876

I don't know how your completion closure looks but you could consider using an enum with an associated value for the result (here you can read more about associated values).

If you create an enum like this:

enum ClosureResult {
   case success([String])
   case failure([String], [Error])
}

you have the ability to return different values dependent on whether your network lookups failed or succeded. Note that I'm passing an array of Error objects as the second parameter in my failure case, I don't know if that suits you but it is just meant to illustrate that you can pass more than one parameter in return.

If you then use the ClosureResult as the return value of you completion closure, you can do a switch on it in your own block when you return and act dependent on the value.

So, your function could look something like this:

func yourFunction(items: [String], completion: (ClosureResult) -> Void) {
    var completedItems = [String]()
    var failedItems = [Error]()
    for item in items {
        //Do your network magic here 
        //if it succeeds add it to completedItems
        //if it fails, add it to failedItems
    }

    //did we encounter any failures?
    if failedItems.count > 0 {
        //yes we did, return a .failure then
        let failure = ClosureResult.failure(completedItems, failedItems)
        completion(failure)
    } else {
        //no...return .success
        let success = ClosureResult.success(completedItems)
        completion(success)
    }
}

And you can then use it like so:

let items = ["a", "b", "c"]
yourFunction(items: items) { result in
    switch result {
    case .success(let okItems):
        print(okItems)
    case .failure(let okItems, let failedItems):
        print(failedItems)
    }
} 

Update

In your comment you ask:

My only concern is that in, in your example, the network request succeeds on "a" and "c" but not "b", the closure result contain any information about which item failed. Would it be reasonable to return tuples of the items and the network result for that item in the ClosureResult object instead of just strings?

And yes, that makes sense. I just used String and Error as an example but you can use your own classes too.

I think, what I'd do was, instead of using a tuple I'd create a simple class and return that.

For instance something like:

struct ClosureError {
    let item: String
    let error: Error
}

And then your ClosureResult enum could use that:

enum ClosureResult {
   case success([String])
   case failure([String], [ClosureError])
}

In your yourFunction you'd need to create a new instance of ClosureError when you encountered a failure:

    ...
    var failedItems = [ClosureError]()
    ...
    for item in items {
        //Do your network magic here 
        //if it succeeds add it to completedItems
        //if it fails, add it to failedItems
        let error = //create the error
        let closureFailure = ClosureFailure(item, error)
        failedItems.append(closureFailure) 
    }
    ...

And finally, in your switch you would know which items had failed:

let items = ["a", "b", "c"]
yourFunction(items: items) { result in
    switch result {
    case .success(let okItems):
        print(okItems)
    case .failure(let okItems, let failedItems):
        for closureError in failedItems {
            print("item: \(closureError.item) has failed with error \(closureError.error)"
        }
    }
} 

You should probably figure out some better names for the various parts :)

Hope that makes sense and can be used.

Upvotes: 2

Related Questions