8 minutes to read
Created by
Calytic
Updated by
Wulf

Promises

Details about the A+ promise implementation in uMod.

This guide is for uMod, not Oxide.
Join our discord for the latest updates and the latest news! Join discord

tl;dr: I'm already familiar with promises in JS

uMod Promises are designed to work just like JS promises with a couple of notable differences.

  1. Promise chains should end with a call to a terminator method; either Done, Fail or Complete.They operate identically to Then, Catch and Finally, respectively. Avoiding unnecessary promises, or using terminator methods, is an optimization that reduces allocations.
  2. The Fail/Catch method is necessary to recover from exceptions occuring within other promise callbacks.

Introduction

A promise represents the result of an asynchronous operation. In most cases promises are created or pooled externally and developers will attach callbacks to already existing promises.

For example, consider asynchronously loading a data file that contains a single integer...

Action<int> successCallback = (int value) =>
{
    // The file was loaded and its content deserialized on a separate background thread
};

Action<Exception> errorCallback = (Exception exception) =>
{
    // Something went wrong
    Logger.Report("Unable to load file", exception);
};

var promise = Files.ReadObject<int>("path/to/file.dat");
promise.Done(successCallback, errorCallback);

In the above example, the file file.dat will be loaded into memory and deserialized in a background thread. Once this process is complete, the successCallback will be invoked (with the result of the fulfilled promise), or the errorCallback will be invoked (in the case of an exception).

Terminology

  • Fulfilled - A fulfilled promise is a promise that has successfully finished.
  • Rejected - A rejected promise is a promise that has finished with a failed state.
  • Settled - A settled promise is a promise that either fulfilled or rejected.
  • Pending - A pending promise is a promise that has not settled.
  • Resolved - A resolved promise is a promise that may not have yet been settled but its outcome has already been determined. This usually means the outcome of this promise will be determined by the outcome of another promise.

Constructing a promise

In the example below, when a player runs the command /test, a promise is created which will resolve when a repeating timer has completed execution.

[Command("test")]
void TestCommand(IPlayer player)
{
    MyTimerPromise(player)
        .Done(delegate()
        {
            player.Reply("Timer executed 5 times over 25 seconds");
        });
}

IPromise MyTimerPromise(IPlayer player)
{
    Promise promise = new Promise();
    int count = 1;
    int recurrences = 5;
    
    timer.Repeat(5f, recurrences, delegate()
    {
        player.Reply($"Timer executed: {count}");
        if (count == recurrences)
        {
            // Timer finished
            promise.Resolve();
        }
        
        count++;
    });
    
    return promise;
}

The above code is an example of deferring a callback until the promised (typically asynchronous) operation is complete and will print...

Timer executed: 1
Timer executed: 2
Timer executed: 3
Timer executed: 4
Timer executed: 5
Timer executed 5 times over 25 seconds

Utility functions

There following utility function available to allow easily handling multiple promise:

  • Promise.All - Wait for all given promises to fulfill.
  • Promise.AllSettled - Wait for all given promises to settle (fulfill or reject).
  • Promise.Any - Wait for the first of given promises to fulfill.
  • Promise.Race - Wait for the first of given promises to settle (fulfill or reject).

The most commonly used of these is Promise.All which can be used like so:

var promise1 = Files.ReadObject<int>("path/to/file1.dat");
var promise2 = Files.ReadObject<int>("path/to/file2.dat");

Promise.All(promise1, promise2).Done(successCallback, errorCallback);

Note: This is more performant than chaining the two promises together as this allows the two promise to execute in parallel.

Chaining

Promises may be chained together into a series of promises which invoke two or more asynchronous operations. Chained promises are called in order and only upon completion of the previous promise.

For example, we may want to perform a web request using the data received from the first async operation.

promise
  .Then((int id) =>
  {
      return Web.Get($"https://httpbin.org/anything/{id}");
  })
  .Then((WebResponse response) =>
  {
      if (response.StatusCode == 200)
      {
          Logger.Info(response.ReadAsString());
      }
  })
  .Fail(errorCallback);

When chaining promises only one error handler is required near the end of the chain. This will catch any errors that happen before it in the chain. Additional error handles can be added up the chain via the Catch method if necessary to recover from earlier callback errors.

Chain Tterminators

There are 3 promise chain terminator methods: Done, Fail and Complete.These are counterparts to their non-terminator methods Then, Catch and Finally respectively.

It is highly recommend to always end a promise chain with a terminator method.

A promise chain terminator is a method designed to be called at the end of a promise chain. Without a final call to one of these methods, if an exception is thrown in one of the promises in the chain (i.e. a promise rejects) and it isn't handled via an errorCallback the exception is quietly ignored.

Using a terminator method indicates that the promise is no longer being used and is safe to dispose of (or free from memory).

This means that after the call it is no longer safe to reference any promises in the chain; doing so may result in a ObjectDisposedException or in the worst case, using a promise that is already being reused elsewhere.

Note: As long as it can be guaranteed that the promise has not yet settled (i.e. it's still pending) then it is safe to keep using it after calling a terminator method but it is not recommended.

Chain transformation

Chained promises may transform the promise parameter and yield it to the next promise in the chain (or the terminator).

For example, simply perform arithmetic using an integer promise and print the result:

promise
.Then(delegate(int result)
{
    return result + 1;
})
.Then(delegate(int newResult)
{
    Logger.Info($"Result was {newResult}");
}).Fail(failCallback);

Change the type by converting/casting the promise result...

promise.Then(delegate(string result)
{
    if (int.TryParse(result, out int newResult))
    {
      return newResult;
    }
    throw new InvalidArgumentException("result");
}).Then(delegate(int result)
{
    // do something with integer
});