Can't get throws to work with function with completion handler
Asked Answered
S

4

18

I'm trying to add a throws to my existing function with a completion handler but I keep getting a warning saying no calls throwing functions occur within try expression. In the section where I throw the errors, I get an error saying

invalid conversion from throwing function of type '() throwing -> Void' to non-throwing function type.

enum LoginError: ErrorType {
    case Invalid_Credentials
    case Unable_To_Access_Login
    case User_Not_Found
}

@IBAction func loginPressed(sender: AnyObject) {

    do{
        try self.login3(dict, completion: { (result) -> Void in

            if (result == true)
            {
                self.performSegueWithIdentifier("loginSegue", sender: nil)
            }
        })
    }
    catch LoginError.User_Not_Found
    {
        //deal with it
    }
    catch LoginError.Unable_To_Access_Login
    {
        //deal with it
    }
    catch LoginError.Invalid_Credentials
    {
        //deal with it
    }
    catch
    {
        print("i dunno")
    }

}

func login3(params:[String: String], completion: (result:Bool) throws -> Void)
{
    //Request set up
    let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
        do {
            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(result:true)
                    }

                }
                else if loginError != ""
                {
                    dispatch_async(dispatch_get_main_queue()){
                        completion(result: false)
                        self.loginErrorLabel.text = loginError
                        throw LoginError.User_Not_Found
                    }
                }
                else if validationError != ""
                {
                    dispatch_async(dispatch_get_main_queue()){
                        completion(result:false)
                        self.validationErrorLabel.text = validationError
                        throw LoginError.Invalid_Credentials
                    }

                }
                else if exception != nil
                {
                    dispatch_async(dispatch_get_main_queue()){
                        completion(result:false)
                        self.exceptionErrorLabel.text = "Unable to login"
                        throw LoginError.Unable_To_Access_Login
                    }
                }
            }
            else
            {
            }
        }
        catch let parseError {
            // Log the error thrown by `JSONObjectWithData`
        })

        task.resume()

}
Slype answered 28/10, 2015 at 21:58 Comment(3)
Thats an async function. In your callback you need to return an error not throw, by the time you throw your program has already returned from that function hence it doesn't work.Queenqueena
I'm a bit confused. Do I want the callback to throw or should the function that contains the callback throw? I'm waiting on the response from the server to determine what type of error to throw, which makes me think the callback needs to throw.Slype
@Victor Sigler answer is what you are looking for.Queenqueena
J
16

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.

Juvenilia answered 28/10, 2015 at 22:21 Comment(1)
Above is not working, if you want to throw the login error in completion block to another function.Suzy
T
2

You're asking for X and I'm answering Y, but just in case...

There's always the possibility to add the throwing capability to your function instead of the completion handler:

func login3(params:[String: String], completion: (result:Bool) -> Void) throws {
    ...
}

Then you can call it from inside IBAction:

do {
    try self.login3(dict) { result -> Void in
        ...
    }
} catch {
    print(error)
}
Tedium answered 28/10, 2015 at 22:20 Comment(4)
I tried your approach but got caught up on one error. At dispatch_async(dispatch_get_main_queue()) { self.loginErrorLabel.text = loginError completion(inner: { throw LoginError.User_Not_Found }) } i'm still getting the error saying invalid conversion from throwing function of type '() throwing -> Void' to non-throwing function type.Slype
@Slype With this version no need to change your completions, keep them as they were (no "inner" stuff). No need for anything else than what I show.Tedium
I'm trying to implement your solution, but If I place the throw within dispatch_async I get the error I described in my previous comment.Slype
I didn't work out all the implications of my proposition, indeed, it was just a hint towards an alternative. :) Victor's anwer is very good anyway, I'd say go with his solution, it looks like it'll fit your requirements.Tedium
H
0

Read the below with a grain of salt. I haven't worked much with Swift 2.0 yet:

You created a "dataTask" task who's's completion handler has a throw in it, but the only actual code in your login3 method is task.resume(). The completion handler won't get executed until after login3 returns. (In fact, it's a parameter to another object, so the compiler has no idea what's going to happen with that code.)

As I understand it, the actual top-to-bottom body of your login3 method must contain a throw. Since it's an async method, you can't do that. Thus, don't make your login3 function throw. Instead have it pass an error object to it's completion handler.

Hardworking answered 28/10, 2015 at 22:9 Comment(0)
T
0

In my impression this is caused by the signature of your functions. In @IBAction func loginPressed(sender: AnyObject) you don't have to use try when calling login3 as the function itself is not marked as throwing but the completion handler is. But in fact the completion closure of login3 will never throw as you execute all throwing functions inside a do {} catch {} block, so you could try to remove the throws annotation from the login3 completion closure and also call the closure if you catch an error in login3 with an according result. This way you handle all throwing functions inside of login3 in a do {} catch {} block and call the completion handler with a suitable value.

Generally I am also not aware that you can catch without a preceding do {} block like you did in the @IBAction.

Taille answered 28/10, 2015 at 22:18 Comment(1)
The throwing in login3 is also unnecessary as you will catch your previously thrown Errors directly in the catch block. What I would recommend is checking against error conditions and call the completion closure with an error or in your case with false as result. If you modify the closure parameters so you can pass an error to the closure you could then check for different error conditions in your @IBAction.Taille

© 2022 - 2024 — McMap. All rights reserved.