util.promise

Promises allow a non-blocking/asynchronous function to return a “promise” of a future value, before it is actually available.

An alternative (and common) solution is for such functions to have a ‘callback’ parameter - calling this callback function with the data when it becomes available.

Promises are more flexible than callbacks, and can be much simpler to use when multiple asynchronous operations are involved (e.g. either nested or in sequence). Although promises are a generic concept, this module is based heavily on the Promise API available in Javascript/ES6, which is already proven in many applications and APIs and may already be familiar to many people.

Usage

Using a function that returns a promise

If an API is documented as returning a promise object, be aware that just like with callback-based APIs, the function will return immediately. To execute code when the operation is complete, you can pass a callback to the ‘next()’ method of the promise object:

local promise = get_user_data("username");
promise:next(function (data)
    print("The user's data is: "..data);
end);

promise:next() automatically returns another promise based on the return value of the callback, which means calls to :next() can be chained:

promise:next(json.decode):next(function (data)
    -- This function is called with the return value of json.decode
    print(data.property);
end);

If the original operation fails, or if one of the callbacks fails, subsequent promises in the chain will go into an error state, and their normal callbacks will not be called. You can handle these errors by supplying an error handling function either as the second parameter of :next(), or to a :catch() method call.

Writing a function that returns a promise

Promises are easy to use with any existing async API. This function demonstrates how an API that takes a callback could be trivially converted into one that returns a promise:

function get(url)
    return promise.new(function (on_fulfilled, on_error)
        assert(http.request(url, nil, function (body, code)
            if code ~= 200 then
                on_error("Failed to fetch, HTTP error code "..code);
                return;
            end
            on_fulfilled(body);
        end));
    end);
end

Functions

promise.new(func)

Creates a new promise and immediately executes the function func, passing it two parameters: on_fulfilled and on_error.

The function should call on_fulfilled(data) (where data is optional and can be any value) when it wants to resolve the promise.

If there is a problem and the promise cannot be fulfilled, calling on_error(err) will execute any error callbacks that have been set on the promise, passing err which again can be any value. Any unhandled errors in func will automatically result in on_error() being called with the uncaught error object.

Example:

function timeout(n)
    return promise.create(function (on_fulfilled)
        timer.add_task(n, on_fulfilled);
    end);
end

promise.resolve(value)

A helper function that returns a promise that is already fulfilled with the given value. That is, if :next(callback) is called on this promise, it would execute callback immediately with the provided value.

promise.reject(err)

Similar to promise.resolve(), except returns a promise that is in an error state. That is, if :next(ok_callback, err_callback) is called, err_callback would immediately be called with the value of err.

promise.race(array_of_promises)

Returns a new promise that fulfills when any of the promises in the passed array are fulfilled, and it fulfills with that value, or errors when any of the promises in the array error. Subsequent changes in any promises are ignored.

Use this function to wait for the first of multiple asynchronous operations. For example, to add a timeout to an asynchronous operation you might write code like the following:

local data = get_data("username");
local timeout = promise.new(function (_, fail)
    timer.add_task(5, function ()
        fail("timeout!");
    end);
end);

promise.race({ data, timeout })
    :next(function (received_data)
        print("The user's data is: "..received_data);
    end)
    :catch(function (err)
        -- err may be "timeout!" or any error from the get_data() promise also
        print("There was an error fetching the user's data: "..err);
    end;

promise.all(table_of_promises)

Returns a new promise that only fulfills when all of the promises in the provided table’s values are fulfilled. It fulfills with a new table that contains the values returned by each of the promises.

Use this function when you want to wait for multiple asynchronous operations to complete successfully, for example by passing an array of promises:

local promises = {};
for i, username in ipairs(users) do
    promises[i] = get_data(username);
end

promise.all(promises)
    :next(function (results)
        for i, username in ipairs(users) do
            print(username, results[i]);
        end
    end)
    :catch(function (err)
        print("Failed to fetch data for one or more users: "..err);
    end);

New in Prosody 0.12: Non-numeric keys are supported (e.g. a table such as { foo = promise1, bar = promise2 }), and values that are not promises will be passed through to the resulting table unchanged.

promise.all_settled(table_of_promises)

Similar to promise.all(), but always resolves successfully even if some of the promises fail (are ‘rejected’).

For each promise in the input table, the output table will have a entry with the same key and the value will be a result table with a status field containing the string "fulfilled" if the promise resolved successfully, or "rejected" if the promise failed to resolve.

For a "fulfilled" promise, the result table will contain a value field with the value that the promise resolved to. For "rejected" promises, the result table will have a reason field with the value that the promise was rejected with (e.g. an error).


promise.all_settled({
    foo = promise.resolve("a successful thing");
    bar = promise.reject("a failed thing");
}):next(function (result)
    for k, v in pairs(result) do
        print(k..":");
        if v.status == "fulfilled" then
            print("  Succeeded: ", v.value);
        elseif v.status == "rejected" then
            print("  Failed: ", v.reason);
        end
    end 
end);

-- Output:
-- foo:
--  Succeeded:  a successful thing
--  bar:
--    Failed:   a failed thing

Arrays and tables with non-numeric keys are supported, and values that are not promises will be passed through to the resulting table unchanged.

This function is new in Prosody 0.12.

promise.join(handler, promises...)

This is a convenience wrapper for promise.all() that is useful when the number of promises is small and fixed. Instead of constructing and deconstructing an array, the results are passed directly as parameters to the given handler function.

promise.join(function (result1, result2)
    print("Result 1: ", result1);
    print("Result 2: ", result2);
end, promise.resolve("foo"), promise.resolve("bar"));

As with promise.all(), this function returns a promise, which will reject if any of the provided promised reject. On success, it will resolve (with the return value of the handler function, if any). It will pass through non-promise parameters to the handler function unchanged.

This function is new in Prosody 0.12.

promise.try(func)

Executes the provided function, and:

  • Converts any non-promise return value to a fulfilled promise with that value
  • Converts any uncaught errors to a failed promise with the uncaught error object
  • Returns a promise bound to the result of a promise returned by func, if it returns a promise

This function is useful for wrapping non-promise code, and acting as a promise-friendly alternative to pcall(), when error-safety is required inside a function that is supposed to always return errors via a promise.

Methods

promise:next(on_fulfilled, on_error)

Set callbacks to be executed when the promise is fulfilled or an error occurs. Only one of these callbacks will be executed, and only once (per promise they are attached to).

This method can be called multiple times on the same promise… callbacks will execute in the order they were attached.

It also returns a new promise which is automatically fulfilled with the return value of the on_fulfilled callback. This allows :next() calls to be chained. If the callback returns a promise object, the result of that returned promise is used to resolve the promise returned from :next() - i.e. the result will be passed to later callbacks in the chain.

Both callbacks are optional - you can provide either without the other.

promise:catch(on_error)

A neater way to set just an error handler for the promise. Identical behaviour to promise:next(nil, on_error).

promise:finally(on_finally)

Adds a callback that will run in both success and failure of the promise.

Note that, unlike on_fulfilled and on_error callbacks:

  • on_finally does not receive the result of the promise (in either the success or failure case).
  • Although :finally() does return a promise (and thus allows chaining like :next() and :catch()), it does not use the return value of the on_finally callback, but the result of the original promise.
  • An exception to the above is in the case of an uncaught error inside on_finally - in this case the promise returned by :finally() will be in an error state and on_fulfilled callbacks later in the chain will not be called.

This method is useful to add some cleanup of resources once the promise is settled one way or another.