Coroutines in Unity – Encapsulating with Promises [Part 2]

In the last article we focused on the internals behind Unity’s Coroutines and went in depth on how they work. We covered IEnumerator interface and iterator blocks to get a grasp on how the engine takes care of the coroutine functions. We’ve also managed to list a few problems with the implementation and shortcomings that you can stumble upon if a need to write more complex coroutines arises.
In today’s article, we’re going to present you the idea of Promises and where they originated from.

Callbacks

Most asynchronous operations in Javascript APIs return a result with a callback (in C# terms: an Action delegate) when they finish work. You can explicitly provide a function for those, but to follow asynchronous code easier many decide to implement callbacks using anonymous functions. In theory it does seem like a decent upgrade to our Coroutines. Let’s try to form an example of how to do that in C#:

StartCoroutine(RequestData( (error, message) =>
{
    if (error)
    {
        Debug.Log(string.Format("We got an error: {0}", message));
    }
    else
    {
        StartCoroutine(RequestSomeOtherData(message, (error2, message2) =>
        {
            if (error2)
            {
                Debug.Log(string.Format("We got an error: {0}", message2));
            }
            else
            {
                Debug.Log(string.Format("Success: {0}", message2));
            }
        }));
 	}
}));
 
// ...
 
IEnumerator RequestData(Action<bool, string> onComplete)
{
    // Do some real work
    yield return new WaitForEndOfFrame();
    //--
 
    // No errors
    onComplete(false, "Data 1 working");
}
 
IEnumerator RequestSomeOtherData(string dataArgument, Action<bool, string> onComplete)
{
    // Do some real work with dataArgument
    yield return new WaitForEndOfFrame();
    //--
 
    // No errors
    onComplete(false, "Data 2 woohoo");
}

Not bad! As you can see that actually works fairly well. It gives us a possibility to return a value as well as raise errors and react to them. The problem, however, is the code growing horizontally with more indents as you stack coroutines in a sequence. Also, the error checking has to be done in each and every one of them. Eventually, with more complicated functions you end up in a callback hell. What is that? Well, let’s just copy our coroutine calls and stack four of these bad boys:

StartCoroutine(RequestData( (error, message) =>
{
    if (error)
    {
        Debug.Log(string.Format("We got an error: {0}", message));
    }
    else
    {
        StartCoroutine(RequestSomeOtherData(message, (error2, message2) =>
        {
            if (error2)
            {
                Debug.Log(string.Format("We got an error: {0}", message2));
            }
            else
            {
                StartCoroutine(RequestSomeOtherData(message, (error3, message3) =>
              	{
                    if (error3)
                    {
                        Debug.Log(string.Format("We got an error: {0}", message3));
                    }
                    else
                    {
                        StartCoroutine(RequestSomeOtherData(message, (error4, message4) =>
                        {
                            if (error4)
                            {
                                Debug.Log(string.Format("We got an error: {0}", message4));
                            }
                            else
                            {
                                Debug.Log(string.Format("Success: {0}", message4));
                            }
                        }));
                    }
                }));
            }
        }));
    }
}));

Just look at the pyramid shape and all the } and })); at the end. Yikes! You’d also need to get really creative with the names as they have to be unique. In terms of errors, you could probably clean it up a bit if you moved error handling to another function. But it still doesn’t look appealing and here we only have four coroutines stacked. When writing more complex functions or perhaps APIs for your backend services, that number can increase drastically. So, what’s the solution?

Promises

Promises have been popularized by many 3rd party Javascript libraries before getting a native implementation in ES6. It’s interesting to note that the idea itself had been actually researched and introduced in the 70s and 80s. Promises as we know them today were first introduced in a year 1988 by Barbara Liskov and Liuba Shira, but a similar idea came from the functional programming languages years before.

A C# library conforming the Promises/A+ Spec was released a couple of years ago and is a big game changer for managing asynchronous operations. Let’s dive into the code.

A Promise is an object that you return from your asynchronous function immediately, so the caller can await resolution (or error) of your operations. Essentially, it represents a proxy for a value not necessarily known when the promise is created. A Promise can be later resolved or rejected once you complete your actions. This allows asynchronous methods to return values and run like synchronous methods: except instead of the final value, they return a promise of having a value at some point in the future. The best part, however, is the simplicity of stacking them with a simple interface. Here’s how we can redo our first example and implement Promises instead of pure Action delegates:

using RSG;
//...

RequestData()
.Then(result =>
{
    return RequestSomeOtherData(result);
})
.Then(result =>
{
    Debug.Log(string.Format("Success: {0}", result));
})
.Catch(error => { // Exception error
    Debug.Log(string.Format("We got an error: {0}", error.Message));
});
 
//...
 
IPromise RequestData()
{
    var promise = new Promise();
    StartCoroutine(_RequestData(promise));
    return promise;
}
 
IEnumerator _RequestData(Promise promise)
{
    // Do some real work
    yield return new WaitForEndOfFrame();
    //--
 
    // No errors
    promise.Resolve("Data 1 working");
}
 
 
IPromise RequestSomeOtherData(string dataArgument)
{
    var promise = new Promise();
    StartCoroutine(_RequestSomeOtherData(dataArgument, promise));
    return promise;
}
 
IEnumerator _RequestSomeOtherData(string dataArgument, Promise promise)
{
    // Do some real work with dataArgument
    yield return new WaitForEndOfFrame();
    //--
 
    // No errors
    promise.Resolve("Data 2 woohoo");
}

Much better on the caller side! You get to hide and encapsulate coroutine behavior while getting all the advantages from the promises. With .Then() function you can easily stack new promises in a logical order and handle different result types throughout them. Moreover, with .Catch() you can catch exceptions in all of your promises. If any of the above gets rejected, then the promise chain will get immediately terminated and the code will jump to the nearest catch. Not only that, the catch function actually uses Exception type, so you can also easily throw your own exceptions:

class MyException : Exception
{
    private readonly int foo;
 
    public int Foo
    {
        get { return foo; }
    }
 
    public MyException(int foo)
    {
       	this.foo = foo;
    }
 
    public MyException(int foo, string message)
        : base(message)
    {
        this.foo = foo;
    }
 
    public MyException(int foo, string message, Exception inner)
        : base(message, inner)
    {
        this.foo = foo;
    }
}
 
IEnumerator _RequestData(Promise promise)
{
    // Do some real work
    yield return new WaitForEndOfFrame();
    //--
 
    // Error!
    promise.Reject(new MyException(100, "Error error!"));
}

As long as you extend from Exception class you can write your own exceptions, mimicking try…catch behavior. Handle them like this:

.Catch(error => { // Exception error
    if (error is MyException)
    {
        var myError = error as MyException;
        Debug.Log(string.Format("We got an error: {0} {1}", myError.Message, myError.Foo));
    }
});

Summary

As you can see, Promises provide a great mechanism to handle your asynchronous operations. You can easily throw errors from any of your promises in stack, handle returning values as well as hide the coroutine implementation if you use it while working with Unity. You retain all the powerful aspects of Coroutines in terms of the implementation, but also improve greatly the interface for your complicated APIs and services. The library offers much more than what was covered and we strongly recommend looking up the documentation to see what else it has to offer.
In the final part of the series, we’ll show you a real example of writing a REST API interface using Unity’s Coroutines & Promises to achieve the cleanest code possible.

Continued in Part 3 here.

related
MultiplayerNakamaTutorial
Tutorial: Making a Multiplayer Game with Nakama and Unity: Part 3/3
Implementing Basic Features In this chapter, we will cover some of the features Nakama has...
0
Augmented RealityBasicsTutorial
Unity AR Tutorial: Augmented Reality Game Development with Vuforia
  Vuforia is an AR platform that provides amazing opportunities for augmented reality...
0
GuideIntermediateTutorial
Coroutines in Unity – Encapsulating with Promises [Part 3]
  In the last part of the series we’re going to build a real example of a REST API...
5
Call The Knights!
We are here for you.
Please contact us with regards to a Unity project below.



The Knights appreciate your decision!
Expect the first news soon!
hire us!