Can't Get Throws to Work with Function with Completion Handler

Can't get throws to work with function with completion handler

What you can do is encapsulating the error into a throwable closure like in the following code to achieve what you want:

func login3(params:[String: String], completion: (inner: () throws -> Bool) -> ()) {

let task = session.dataTaskWithRequest(request, completionHandler: { data, response, error -> Void in

let json = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves) as? NSDictionary

if let parseJSON = json {
let userID = parseJSON["user_id"] as? Int
let loginError = parseJSON["user_not_found"] as? String
let validationError = parseJSON["invalid_credentials"] as? String
let exception = parseJSON["unable_to_access_login"] as? String

var responseArray = [(parseJSON["user_id"] as? Int)]
if userID != nil {
dispatch_async(dispatch_get_main_queue()) {
completion(inner: { return true })
}

}
else if loginError != ""
{
dispatch_async(dispatch_get_main_queue()) {
self.loginErrorLabel.text = loginError
completion(inner: { throw LoginError.User_Not_Found })
}
}
else if validationError != ""
{
dispatch_async(dispatch_get_main_queue()) {
self.validationErrorLabel.text = validationError
completion(inner: {throw LoginError.Invalid_Credentials})
}
}
else if exception != nil
{
dispatch_async(dispatch_get_main_queue()){
self.exceptionErrorLabel.text = "Unable to login"
completion(inner: {throw LoginError.Unable_To_Access_Login})
}
}
}
else
{
}
}

task.resume()
}

And the you can call it like in the following way:

self.login3(dict) { (inner: () throws -> Bool) -> Void in
do {
let result = try inner()
self.performSegueWithIdentifier("loginSegue", sender: nil)
} catch let error {
print(error)
}
}

The trick is that the login3 function takes an additional closure called 'inner' of the type () throws -> Bool. This closure will either provide the result of the computation, or it will throw. The closure itself is being constructed during the computation by one of two means:

  • In case of an error: inner: {throw error}
  • In case of success: inner: {return result}

I strongly recommend you an excellent article about using try/catch in async calls Using try / catch in Swift with asynchronous closures

I hope this help you.

Swift throw from closure nested in a function

When you define closure that throws:

enum MyError: ErrorType {
case Failed
}

let closure = {
throw MyError.Failed
}

then type of this closure is () throws -> () and function that takes this closure as parameter must have the same parameter type:

func myFunction(completion: () throws -> ()) {
}

It this function you can call completion closure synchronous:

func myFunction(completion: () throws -> ()) throws {
completion()
}

and you have to add throws keyword to function signature or call completion with try!:

func myFunction(completion: () throws -> ()) {
try! completion()
}

or asynchronous:

func myFunction(completion: () throws -> ()) {
dispatch_async(dispatch_get_main_queue(), { try! completion() })
}

In last case you will not be able to catch error.

So if completion closure in eventStore.requestAccessToEntityType method and the method itself does not have throws in its signature or if completion is called asynchronously then you can not throw from this closure.

I suggest you the following implementation of your function that passes error to callback instead of throwing it:

func insertEventToDefaultCalendar(event: EKEvent, completion: CalendarEventError? -> ()) {
let eventStore = EKEventStore()
switch EKEventStore.authorizationStatusForEntityType(.Event) {
case .Authorized:
do {
try insertEvent(eventStore, event: event)
} catch {
completion(CalendarEventError.Failed)
}

case .Denied:
completion(CalendarEventError.AccessDenied)

case .NotDetermined:
eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
if granted {
//insertEvent(eventStore)
} else {
completion(CalendarEventError.AccessDenied)
}
})
default:
}
}

Throw error from dataTask completionHandler

Short story: You can't throw in a dataTask completion closure

You could return two values in the completion handler

...completion: @escaping (ResultType?, Error?)->Void

and return

completion(result, nil)
completion(nil, CustomError.myerror)

or more convenient use an enum with associated type

enum Result {
case success(ResultType), failure(Error)
}

...completion: @escaping (Result)->Void

and return

completion(.success(result))
completion(.failure(CustomError.myerror))

You can process the result

foo() { result in
switch result {
case .success(let resultType): // do something with the result
case .failure(let error): // Handle the error
}
}

Update:

In Swift 5 using the new built-in Result type it's even more comfortable because Result can capture the result of the throwing expression

...completion: @escaping (Result<MyType,Error>)->Void

let task = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: {
(data, response, error) in

completion(Result { try something()})
})
task.resume()

Update 2:

With async/await completion handlers are gone

do {
let (data, response) = try await URLSession.shared.data(for: request)
} catch {
throw CustomError.myerror
}

How to raise an Error for the completionHandler?

Your function dataTask(with:completionHandler:) does not seem to be marked with throws/rethrows in which case it seems the error is simply being passed to the completion block.

Since neither the function nor the closure "throws", you can't throw the error & as a result neither will you be able to do-try-catch it later anyways.

So as per my assumptions, just the following should suffice:

nextError = MyError.RuntimeError("test")

i.e.

enum MyError : Error {
case RuntimeError(String)
}

func dataTask(with request: NSURLRequest,
completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {

nextError = MyError.RuntimeError("test")

completionHandler(nextData,
successHttpURLResponse(request: request),
nextError)

return nextDataTask as URLSessionDataTaskProtocol
}

Pass function to completion handler

If you want to pass your own closure as an input argument to UIView.animate, you need to match the types of the closures, so myCompletionHandler has to have a type of ((Bool) -> ())?, just like completion.

func hide(animated: Bool, myCompletionHandler: ((Bool) -> ())?) {
if animated {
transform = CGAffineTransform(scaleX: 0.7, y: 0.7)

UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [], animations: {
self.alpha = 0
self.transform = CGAffineTransform.identity
}, completion: myCompletionHandler) // I want to run 'myCompletionHandler' in this completion handler
}
}

This is how you can call it:

hudView.hide(animated: true, myCompletionHandler: { success in
//animation is complete
})

How completion handler work under the hood

Yes, that's how it works if your thread blocking works correctly (i.e. the breakpoint hits that line after all your async stuff is done).

Here, completionHandler is a value type called a closure in Swift. It behaves kind of like lambdas/anonymous functions/etc. (The "kind of" refers to how it plays with Swift's garbage collection, ARC, which works differently from other popular languages. That's a separate topic though.).

These closures are essentially function pointers, so you are basically injecting a function as a parameter that can be used within your function.

In other words, it's like giving your function a box with a button to press. Your function can press that button whenever it wants as long as it can provide the necessary inputs. In this case, a URLResponse? is needed to press that button. When that button is pressed, the caller of this function will have whatever code block it defined executed (synchronously, or sometime in the future).

Because your question involves "under the hood"
I recommend reading some Swift [documentation][1]. If there's something you don't understand, go ahead and comment here. Other people spent hours refining that page with great details and behaviors that I can't reasonably mimic here.

Pro tip: Don't forget to use [weak self]!!!

COMMENT RESPONSE:

thank you very much, but I still have one more question: you said "if your thread blocking works correctly", but in ordinary case, will it happen? I mean, if I do nothing about blocking thread, adding break point,... is it happen? - phuongzzz

Let's look at this example:

//You can copy-paste the following into a playground
import Foundation

//MARK:- FUNCTIONS

//Here's a buggy function
func intentionallyBuggyFunction(onDone: @escaping (String) -> ())
{
var resultTxt = "Oops the async did not happen"

DispatchQueue.main.asyncAfter(deadline: .now() + 3)
{
resultTxt = "The async happened!"
}
onDone(resultTxt)
}
//Here's one that'll async properly (this example is just for demonstration purposes)
func thisOneWillWork(onDone: @escaping (String) -> ())
{
let thread = DispatchQueue(label: "Bambot", qos: .userInteractive, attributes: .concurrent,
autoreleaseFrequency: .workItem, target: nil)
thread.async {[onDone]
var resultTxt = "Oops the async did not happen"
let dg = DispatchGroup()

dg.enter()
thread.asyncAfter(deadline: .now() + 3)
{[weak dg] in
resultTxt = "The async happened!"
dg?.leave()
}
dg.wait()
onDone(resultTxt)
}
}

//MARK:- CODE EXECUTION

//This'll print the wrong result
intentionallyBuggyFunction { (resultStr) in
print(resultStr)
}
//This'll print the right result
thisOneWillWork { (resultStr) in
print(resultStr)
}

As you can see, the first function will queue the async stuff, immediately execute the closure, and then hit its ending function brace. The function finishes executing before the async stuff can happen even though the closure is escaping.

The second function actually instructs the device to wait for pending updates on a separate thread. That way, the String is updated before the closure is called.

Now, the second function is, I would consider, spaghetti code. It's not good practice to just willy-nilly spin up a DispatchQueue like this because iOS only has so many threads in its pool to choose from. It's also misusing DispatchGroup (for behavior like this, you should make a serial queue (you can Google that)).

It's better to just capture the completion handler {[onDone](...) in and then call it inside the actual async block like so:

//Better async (cleaner, makes more sense, easier to read, etc.)
func betterAsync(onDone: @escaping (String) -> ())
{
var resultTxt = "Oops the async did not happen"

DispatchQueue.main.asyncAfter(deadline: .now() + 3)
{[onDone] in
resultTxt = "The async happened!"
onDone(resultTxt)
}
}

Invalid conversion from throwing function of type '(_, _) throws - ()' to non-throwing function type '(Bool, Error?) - Void

You cannot throw from within the non throwing completion handler. It is asynchronous! Just as you cannot return a value from within an asynchronous function, so too you cannot throw there. After all, to catch the throw, the throw would need to travel backwards in time to when the original function was called. And that is metaphysically impossible.

Instead you must provide another completion handler such that you can call it with an Error parameter from within the non throwing completion handler. That is exactly why the Result type was invented, to allow you to propagate an error through an asynchronous situation.

How to call throw in nested non-throwable closure?

If you can't change the function getOptionalConfigurations then you can't throw from inside function because nothing will process the error that you throw.

But if you want to throw the error from the outer function without changing the existing architecture I can propose only a very bad solution:

func retrieveConfigurations(_ completion:@escaping (([String]?) throws -> Void)) throws {
let d = DispatchGroup()
var errorToThrow: Error?

d.enter()
getOptionalConfigurations { (configurations: [String]?, error: Error?) in
do {
try completion(configurations)
d.leave()
} catch {
errorToThrow = error
d.leave()
}
}

d.wait(timeout: .now() + 10)

if let error = errorToThrow {
throw error
}
}

That's bad because if getOptionalConfigurations is executed async then current thread will have to wait for it's completion. I also added timeout for 10 seconds. You can change that to infinity which will make it even worse or just change the number of seconds.



Related Topics



Leave a reply



Submit