Returning a value from asynchronous code

By now, you’ve probably read the posts about what “asynchronous” means and how to use a value set by asynchronous code. If you haven’t, please read them now!

So you understand why you can’t use a value set by asynchronous code. Well, that means you can’t return a value from asynchronous code either!

Here’s what I mean. This example comes almost directly from a question asked on Stack Overflow:

func getJSON(urlstring:String) -> String {
    let url = URL(string: urlstring)
    let request = URLRequest(url: url!)
    var thetitle: String = "noerror"
    let session = URLSession(configuration: URLSessionConfiguration.default)
    let task = session.dataTask(with:request) { (data, response, error) -> Void in
        if error == nil {
            let json = try! JSONDecoder().decode(Person.self, from: data!)
            thetitle = json.title
        }
    }
    task.resume()
    return thetitle
}

This programmer wants do some networking and then return (from his getJSON method) a value set by the dataTask completion handler. He’s upset because, when he says return thetitle, his local variable, thetitle, is returned as a placeholder, "noerror". To track this down, he has even added some logging with print (not shown here) and he can see that, after he fetches and decodes his JSON, when he says thetitle = json.title, he is setting thetitle to a real value. But that real value is not what is being returned from getJSON!

If you understand what “asynchronous” means, you can see the reason for the problem. Things are happening in the wrong order:

    // 1
    var thetitle: String = "noerror"
    // 2
    let session = URLSession(configuration: URLSessionConfiguration.default)
    // 3
    let task = session.dataTask(with:request) { (data, response, error) -> Void in
        // 6, much later!
        if error == nil {
            // 7!
            let json = try! JSONDecoder().decode(Person.self, from: data!)
            // 8!
            thetitle = json.title
        }
    }
    // 4
    task.resume()
    // 5
    return thetitle

Clearly, this is a variant on the problem of using a value after it has been set by asynchronous code. This programmer is not just trying to use a value after it has been set by asynchronous code; he’s trying to return that value after it has been set by asynchronous code. And that’s impossible (unless you’ve got a time machine). This method is returning — coming to an end — before the asynchronous code has a chance to execute and set the local variable thetitle.

That’s right. The main code says return thetitle and ends before the asynchronous code even starts. That’s what asynchronous means!

So how can you return a value from asynchronous code? You can’t. When we look at the declaration of this programmer’s getJSON method, we can see that it is doomed to failure right from the start:

func getJSON(urlstring:String) -> String {

Returning a String from a method is synchronous. But the String he wants to return is obtained asynchronously, meaning after the method has returned!

Completion handler to the rescue

So what’s the solution? First off, do not let evil thoughts start to crowd into your head. “I know, I’ll wait for the networking to finish before returning!” Or, “I know, I’ll network synchronously!” No, no, no, no! All of that is just wrong. Banish it from your mind.

There is a right way to accomplish pretty much the same goal. It takes some preparation, but it is the only correct and coherent way to do this — use another completion handler.

func getJSON(urlstring:String, completionHandler: @escaping (String) -> ()) {

Here we are saying: “Hey, caller! You hand me a URL string, and I’ll do some networking for you and get the result. And you also hand me a function that I can call — a callback function. So, after the networking happens (whenever that may be), I promise to call the function you handed me.”

The completionHandler function here is often referred to a callback, and for a good reason. If you phone someone and they are too busy to talk to you right now, they might say: “Don’t worry, I’ll call you back later when I’m free.” That’s what this approach does. Our getJSON method is promising to call back at some unknown future time, whenever the networking finishes.

So now let’s write the actual getJSON method. It’s just like our original method, except for two things:

  • Our getJSON method no longer returns a String value — or any value. It can’t do that, as we’ve already established.

  • Instead, our getJSON method calls the completion handler function after the networking has finished. And as you already know, the only way to do that is to do it inside the asynchronous code, like this:

func getJSON(urlstring:String, completionHandler: @escaping (String) -> ()) {
    let url = URL(string: urlstring)
    let request = URLRequest(url: url!)
    var thetitle: String = "noerror"
    let session = URLSession(configuration: URLSessionConfiguration.default)
    let task = session.dataTask(with:request) { (data, response, error) -> Void in
        if error == nil {
            let json = try! JSONDecoder().decode(Person.self, from: data!)
            thetitle = json.title
            // HERE is the callback!
            completionHandler(thetitle)
        }
    }
    task.resume()
}

Presto, the problem is solved! Our method receives a URL string, does some networking, receives the JSON, decodes it, pulls out the title, and calls back using the function it was handed, handing the title as a parameter into the callback function.

One final warning

Excellent. But before we end this little discussion, there’s one thing to watch out for. Remember, the networking completion handler is asynchronous. That means the callback is asynchronous too! To see what I mean, look at this from the perspective of the caller, who might say something like this:

self.getJSON(urlstring: "myAPIURLString") { (thetitle) in
    print(thetitle)
    // do something with thetitle here!
}

It all works fine: we call getJSON with our urlstring, and the result comes back to us as thetitle. But it comes back asynchronously. So all the same warnings about what “asynchronous” means apply here as well!

And, of course, we might never be called back, because the networking might fail and the test if error == nil won’t pass.

As long as you keep those things in mind, you’ll be fine.

Congratulations, you are now an expert on asynchronous code!