ZbadhabitZ
ZbadhabitZ

Reputation: 2913

Does a Completion Handler end the function?

Perhaps I do not understand the concept of a completion handler, but I have a function like so;

func someFunction(completion: @escaping (Bool, LoginError?) -> Void) {
    self.checkDevice() { allowed, error in
       if let e = error {
          completion(false, e)
       }
        completion(true, nil)
    }
}

While being light on what checkDevice() does, the basic premise is that it performs an asynchronous network call, and returns either true with no error (nil), or returns false with an error.

When I run this code, I am finding that the completion handler is being called twice. It sends a completion as a tuple (as false, error) and also as (true, nil). I've done some debugging and there seems to be no manner in which someFunction() is called twice.

I was of the belief that once a completion is sent, the function would end. In my test case, I am forcing an error from checkDevice(), which should result in me sending the completion as (false, error), but I am seeing both (false, error) and (true, nil). Does a completion not immediately end the function?

Upvotes: 1

Views: 1365

Answers (3)

craft
craft

Reputation: 2135

A completion handler does not end a function. The typical way to end a function is to insert a return.

See comment re: your specific case

Upvotes: 1

matt
matt

Reputation: 534893

I was of the belief that once a completion is sent, the function would end.

No, why would that be? When you call this function it’s like calling any other function. The name has no magic power.

Rewrite it like this:

func someFunction(completion: @escaping (Bool, LoginError?) -> Void) {
    self.checkDevice() { allowed, error in
        if let e = error {
            completion(false, e)
            return // *
        }
        completion(true, nil)
    }
}

Actually, I should mention that this is a poor way to write your completion handler. Instead of taking two parameters, a Bool and an Optional LoginError to be used only if the Bool is false, it should take one parameter — a Result, which carries both whether we succeeded or failed and, if we failed, what the error was:

func someFunction(completion: @escaping (Result<Void, Error>) -> Void) {
    self.checkDevice() { allowed, error in
        completion( Result {
            if let e = error { throw e }
        })
    }
}

As you can see, using a Result as your parameter allows you to respond much more elegantly to what happened.

Indeed, checkDevice itself could pass a Result into its completion handler, and then things would be even more elegant (and simpler).

Upvotes: 5

slushy
slushy

Reputation: 12385

Consider including parameter names (for reference). And the completion should only be called once. You can do that using return or by using a full if-else conditional.

func someFunction(completion: @escaping (_ done: Bool, _ error: LoginError?) -> Void) {

    checkDevice() { (allowed, error) in

        if let e = error {
            completion(false, e)
        } else {
            completion(true, nil)
        }

    }

}

func someFunction(completion: @escaping (_ done: Bool, _ error: LoginError?) -> Void) {

    checkDevice() { (allowed, error) in

        if let e = error {

            completion(false, e)
            return

        }

        completion(true, nil)

    }

}

By giving parameters names, when calling the function, you can now reference the signature to denote the meanings of its parameters:

someFunction { (done, error) in

    if let error = error {
        ...
    } else if done {
        ...
    }

}

Upvotes: 1

Related Questions