Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to throw errors from asynchronous closures in Swift 3?

I’m executing some functions in a test asynchronously using a DispatchQueue like this:

let queue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated)
let group: DispatchGroup = DispatchGroup()

func execute(argument: someArg) throws {
  group.enter()
  queue.async {
    do {
      // Do stuff here 
      group.leave()
    } catch {
       Log.info(“Something went wrong")
    }
  }
  group.wait()
}

Sometimes the code inside the do block can throw errors, that I have to catch later on. Since I’m developing a test, I want it to fail, if the code inside the do block throws an error. Is there a way to throw an error, without catching it inside the queue.async call?

like image 244
Alex Moreen Avatar asked Apr 10 '17 20:04

Alex Moreen


Video Answer


2 Answers

You cannot throw an error, but you can return an error:

First, you need to make your calling function asynchronous as well:

func execute(argument: someArg, completion: @escaping (Value?, Error?)->()) {
  queue.async {
    do {
      // compute value here:
      ...
      completion(value, nil)
    } catch {
       completion(nil, error)
    }
  }
}

The completion handler takes a parameter which we could say is a "Result" containing either the value or an error. Here, what we have is a tuple (Value?, Error?), where Value is the type which is calculated by the task. But instead, you could leverage a more handy Swift Enum for this, e.g. Result<T> or Try<T> (you might want to the search the web).

Then, you use it as follows:

execute(argument: "Some string") { value, error in
    guard error == nil else {
        // handle error case
    }
    guard let value = value else {
        fatalError("value is nil") // should never happen!
    }
    // do something with the value
    ...
}

Some rules that might help:

  1. If a function calls an asynchronous function internally, it inevitable becomes an asynchronous function as well. *)

  2. An asynchronous function should have a completion handler (otherwise, it's some sort of "fire and forget").

  3. The completion handler must be called, no matter what.

  4. The completion handler must be called asynchronously (with respect the the caller)

  5. The completion handler should be called on a private execution context (aka dispatch queue) unless the function has a parameter specifying where to execute the completion handler. Never use the main thread or main dispatch queue - unless you explicitly state that fact in the docs or you intentionally want to risk dead-locks.

*) You can force it to make it synchronous using semaphores which block the calling thread. But this is inefficient and really rarely needed.

Well, you might conclude, that this looks somewhat cumbersome. Fortunately, there's help - you might look for Future or Promise which can nicely wrap this and make the code more concise and more comprehensible.

Note: In Unit Test, you would use expectations to handle asynchronous calls (see XCTest framework).

like image 198
CouchDeveloper Avatar answered Oct 17 '22 22:10

CouchDeveloper


Refactor your code to use queue.sync and then throw your error from there. (Since your execute function is actually synchronous, given the group.wait() call at the last line, it shouldn't really matter.)

For instance, use this method from DispatchQueue:

func sync<T>(execute work: () throws -> T) rethrows -> T

By the way, a good idiom for leaving a DispatchGroup is:

defer { group.leave() }

as the first line of your sync/async block, which guarantees you won't accidentally deadlock when an error happens.

like image 35
Paulo Mattos Avatar answered Oct 17 '22 21:10

Paulo Mattos