Working with Promises

In IceLink 2, the preferred way to handle asynchronous operations was parameter objects with a number of callbacks. This worked well initially, as the API had a limited number of well-defined operations. As IceLink has matured, however, its feature set has grown. As more operations were added to the core API, more callbacks and event handlers had to be added to accommodate them. The end result was a large number of functions and parameter objects whose only purpose was to notify the user when some operation had completed or had failed.

IceLink 3 has moved to a Promise-based API. This brings the API more into line with modern thinking on asynchronous operations and allows for a more fluid API with less boilerplate. To demonstrate this, look at the following section of IceLink 3 code that accepts an offer and creates an answer.

connection.SetRemoteDescription(offer).Then(offer =>
{
    return connection.createAnswer();
}).Then(answer => 
{
    return connection.setLocalDescription(answer);
}).Fail(ex =>
{
    Console.WriteLine(ex.Message);
});

The same code, if it were written in IceLink 2 style would look like this:

connection.setRemoteDescription(new SetRemoteDescriptionArgs
{
    Offer = offer,
    OnSuccess = e =>
    {
        connection.CreateAnswer(new CreateAnswerArgs
        {
            OnSuccess = e =>
            {
                connection.SetLocalDescription(e.Answer);
            }
            OnFailure = e =>
            {
                Console.WriteLine(e.Exception.Message);
            }
        }
    }
    OnFailure = e =>
    {
        Console.WriteLine(e.Exception.Message);
    }
}

What you'll immediately notice is that the IceLink 3 code is much more readable. The operations are clearly ordered, there is no unnecessary nesting and an error operation needs to be defined only once.

Using a Simple Promise

Now that you're familiar with the reasons behind the switch, let's look at what exactly a Promise is. A Promise is a contract associated with an asynchronous operation. If this operation completes successfully, we say that the promise is "resolved". If this operation does not complete successfully, we say that it was "rejected." These terms are analogous to the old OnSuccess and OnFailure callbacks

Examine the following sample of code.


var promise = new FM.IceLink.Promise<string>();
promise.Then(result =>
{
    Console.WriteLine(result);
});
fm.icelink.Promise<String> promise = new fm.icelink.Promise<String>();
promise.then(result -> {
    System.out.println(result);
});
FMIceLinkPromise<NSString *>* promise = [FMIceLinkPromise promise];
[promise thenWithResolveActionBlock: ^(NSString* result) {
    NSLog(result);
}];
var promise = FMIceLinkPromise()
promise.then(resolveActionBlock: { (result:String) in
    print(result)
})
var promise = new fm.icelink.Promise();
promise.then(function(result) {
    console.log(result);
});

This code creates a new FM.IceLink.Promise object, and specifies that the promise's result type is a string. That is, whatever asynchronous action that this promise represents, the output of this action is expected to be a string. The Then method specifies that when the result is available, the block of code specified should be executed using the result of the operation.

When the asynchronous operation completes, the promise can be resolved by invoking the Resolve method. This is demonstrated below:

promise.Resolve("promise resolved");
promise.resolve("promise resolved");
[promise resolveWithResult: @"promise resolved"];
promise.resolve(result: "promise resolved")
promise.resolve("promise resolved");

When this code executes, the string "promise resolved" will be set as the result and the code blocks that were previously assigned using the Then method will be executed with this string. In the context of the above code, this means that the string "promise resolved" will be printed to the console.

Chaining Promises

One of the advantages of promises is that they can be used to create chains of asynchronous events that depend on each other. This is achieved by using the overload of the Then method. This method takes one generic type parameter, which indicates the type of result that the next promise in the chain returns.

In the following code, assume that a function AsyncPrint exists, which prints a value to the console asynchronously. Examine how it's used to chain promises together.

AsyncPrint("one").Then(result =>
{
    return AsyncPrint("two");
}).Then(result => 
{
    return AsyncPrint("three");
});
asyncPrint("one").then(result -> {
    return asyncPrint("two");
}).then(result -> {
    return asyncPrint("three");
});
[[[asyncPrintWithValue: @"one"] thenWithResolveActionBlock: ^(NSString* result) {
    return [asyncPrintWithValue: @"two"];
}] thenWithResolveActionBlock: ^(NSString* result) {
    return [asyncPrintWithValue: @"three"];
}];
asyncPrint("one").then(resolveActionBlock: { (result:String) -> FMIceLinkPromise in
    return asyncPrint("two")
}).then(resolveActionBlock: { (result:String) -> FMIceLinkPromise in
    return asyncPrint("three")
})
asyncPrint("one").then(function(result) {
    return asyncPrint("two");
}).then(function(result) {
    return asyncPrint("three");
});

In this above example, the lines "one", "two" and "three" will be printed to the console in order. As can be seen here, the result of each promise does not have to be used. Examine, instead, a case where each result is consumed by the next promise.

AsyncPrint("one").Then(result =>
{
    return AsyncPrint(result + " two");
}).Then(str => 
{
    return AsyncPrint(result + " three"):
});
asyncPrint("one").then(result -> {
    return asyncPrint(result + " two");
}).then(str -> {
    return asyncPrint(result + " three");
});
[[[asyncPrintWithValue: @"one"] thenWithResolveActionBlock: ^(NSString* result) {
    return [asyncPrintWithValue: [NSString stringWithFormat: @"%@ two", result]];
}] thenWithResolveActionBlock: ^(NSString* result) {
    return [asyncPrintWithValue: [NSString stringWithFormat: @"%@ three", result]];
}];
asyncPrint("one").then(resolveActionBlock: { (result:String) -> FMIceLinkPromise in
    return asyncPrint(result + " two")
}).then(resolveWithActionBlock: { (result:String) -> FMIceLinKPromise in
    return asyncPrint(result + " three")
})
asyncPrint("one").then(function(result) {
    return asyncPrint(result + " two");
}).then(function(str) {
    return asyncPrint(result + " three");
});

This output will be slightly different. The lines will read "one", "one two" and "one two three". Each promise appends to the result from the previous promise.

One final use case to examine is the case where each Promise has a different type of return value. Note in the following code, the AsyncPrint method has been modified to take one generic parameter.

AsyncPrint<Object1>(new Object1()).Then<Object2>((Object1 o) =>
{
    return AsyncPrint<Object2>(new Object2());
}).Then<Object3>((Object2 o) =>
{
    return AsyncPrint<Object3>(new Object3());
});
asyncPrint<Object1>(new Object1()).then<Object2>((Object1 o) -> {
    return asyncPrint<Object2>(new Object2());
}).then<Object3>((Object2 o) -> {
    return asyncPrint<Object3>(new Object3());
});
[[[asyncPrintWithValue: [Object1 new]] thenWithResolveActionBlock: ^(Object1 o) {
    return [asyncPrint [Object2 new]];
}] thenWithResolveActionBlock: ^(Object2 o) {
    return [asyncPrint [Object3 new]];
}];
asyncPrint(value: Object1()).then(resolveActionBlock: { (o:Object1) -> FMIceLinKPromise in
    return asyncPrint(Object2())
}).then(resolveActionBlock: { (o:Object2) -> FMIceLinkPromise in
    return asyncPrint(Object3())
})
asyncPrint(new Object1()).then(function(o) {
    return asyncPrint(new Object2());
}).then(function(o) {
    return asyncPrint(new Object3());
});

The above code will print out each object in turn. You can specify any chain of objects that makes sense for your application, though in general it's better to keep things simple.

Handling Errors

Proper error handling is an important part of the promise API. Depending on the source languages, Promises can execute in a different scope, which can cause exceptions to be swallowed. This means that parts of your app could fail without any notification. Fortunately, it's simple to add an error handler to any promise or chain of promises.

At any point in setting up a chain of promises, you can append an error handler using the Fail method. The fail method specifies a block of code to run if the promise is rejected or an exception is thrown in a Then block. The failure handler takes a single Exception parameter:

var promise = new FM.IceLink.Promise<string>();
promise.Then(result =>
{
    Console.WriteLine(result);
}).Fail(ex =>
{
    Console.WriteLine(ex.Message);
});
fm.icelink.Promise<string> promise = new fm.icelink.Promise<string>():
promise.then(result -> {
    System.out.println(result);
}).fail(ex -> {
    System.out.println(ex.getMessage());
});
FMIceLinkPromise* promise = [FMIceLinkPromise promise];
[[promise thenWithResolveActionBlock: ^(NSString* result) {
    NSLog(@"%@", result);
}] failWithRejectActionBlock: ^(NSException* ex) {
    NSLog(@"%@", ex.reason);
}];
var promise = FMIceLinkPromise()
promise.then(resolveActionBlock: { (result:String) in
    print(result)
}).then(rejectActionBlock: { (ex:NSException) (
    print(ex.reason)
})
var promise = new fm.icelink.Promise();
promise.then(function(result) {
    console.log(result);
}).fail(function(result) {
    console.log(err.message);
});

The code above will print the exception error message to the console if a promise is rejected. A promise can be rejected either by throwing an exception in the Then block, or by invoking the reject method:

promise.Reject(new Exception("Promise failed."));
promise.reject(new Exception("Promise failed."));
[promise rejectWithException: [NSException exceptionWithName: @"Exception" reason: @"Promise failed."]];
promise.reject(NSException(name: "Exception", reason: "Promise failed."))
promise.reject(new Error("Promise failed."));

The above code will reject the promise, and will output the error message to the console. Again, any exceptions thrown in a Then block will also implicitly reject the promise.

Wrapping Up

The promise API is simple and flexible. It's used everywhere in IceLink 3, so learning how it works will help you with every aspect of the SDK. You can browse through the other guides to see some more practical examples of its use in the IceLink SDK.