NAME

Promise::ES6 - ES6-style promises in Perl

SYNOPSIS

use Promise::ES6;

# OPTIONAL. And see below for other options.
Promise::ES6::use_event('IO::Async', $loop);

my $promise = Promise::ES6->new( sub {
    my ($resolve_cr, $reject_cr) = @_;

    # ..
} );

my $promise2 = $promise->then( sub { .. }, sub { .. } );

my $promise3 = $promise->catch( sub { .. } );

my $promise4 = $promise->finally( sub { .. } );

my $resolved = Promise::ES6->resolve(5);
my $rejected = Promise::ES6->reject('nono');

my $all_promise = Promise::ES6->all( \@promises );

my $race_promise = Promise::ES6->race( \@promises );

my $allsettled_promise = Promise::ES6->allSettled( \@promises );

DESCRIPTION

Coverage Status

This module provides a Perl implementation of promises, a useful pattern for coordinating asynchronous tasks.

Unlike most other promise implementations on CPAN, this module mimics ECMAScript 6’s Promise interface. As the SYNOPSIS above shows, you can thus use patterns from JavaScript in Perl with only minimal changes needed to accommodate language syntax.

This is a rewrite of an earlier module, Promise::Tiny. It fixes several bugs and superfluous dependencies in the original.

STATUS

This module is in use in production and, backed by a pretty extensive set of regression tests, may be considered stable.

INTERFACE NOTES

COMPATIBILITY

This module considers any object that has a then() method to be a promise. Note that, in the case of Future, this will yield a “false-positive”, as Future is not compatible with promises.

(See Promise::ES6::Future for more tools to interact with Future.)

EXPERIMENTAL: ASYNC/AWAIT SUPPORT

This module implements Future::AsyncAwait::Awaitable. This lets you do nifty stuff like:

use Future::AsyncAwait;

async sub do_stuff {
    my $foo = await fetch_number_p();

    # NB: The real return is a promise that provides this value:
    return 1 + $foo;
}

my $one_plus_number = await do_stuff();

… which roughly equates to:

sub do_stuff {
    return fetch_number_p()->then( sub { 1 + $foo } );
}

do_stuff->then( sub {
    $one_plus_number = shift;
} );

UNHANDLED REJECTIONS

This module’s handling of unhandled rejections has changed over time. The current behavior is: if any rejected promise is DESTROYed without first having received a catch callback, a warning is thrown.

SYNCHRONOUS VS. ASYNCHRONOUS OPERATION

In JavaScript, the following …

Promise.resolve().then( () => console.log(1) );
console.log(2);

… will log 2 then 1 because JavaScript’s then() defers execution of its callbacks until between iterations through JavaScript’s event loop.

Perl, of course, has no built-in event loop. This module accommodates that by implementing synchronous promises by default rather than asynchronous ones. This means that all promise callbacks run immediately rather than between iterations of an event loop. As a result, this:

Promise::ES6->resolve(0)->then( sub { print 1 } );
print 2;

… will print 12 instead of 21.

One effect of this is that Promise::ES6, in its default configuration, is agnostic regarding event loop interfaces: no special configuration is needed for any specific event loop. In fact, you don’t even need an event loop at all, which might be useful for abstracting over whether a given function works synchronously or asynchronously.

The disadvantage of synchronous promises—besides not being quite the same promises that we expect from JS—is that recursive promises can exceed call stack limits. For example, the following (admittedly contrived) code:

my @nums = 1 .. 1000;

sub _remove {
    if (@nums) {
        Promise::ES6->resolve(shift @nums)->then(\&_remove);
    }
}

_remove();

… will eventually fail because it will reach Perl’s call stack size limit.

That problem probably won’t affect most applications. The best way to avoid it, though, is to use asynchronous promises, à la JavaScript.

To do that, first choose one of the following event interfaces:

Then, before you start creating promises, do this:

Promise::ES6::use_event('AnyEvent');

… or:

Promise::ES6::use_event('Mojo::IOLoop');

… or:

Promise::ES6::use_event('IO::Async', $loop);

That’s it! Promise::ES6 instances will now work asynchronously rather than synchronously.

Note that this changes Promise::ES6 globally. In IO::Async’s case, it won’t increase the passed-in IO::Async::Loop instance’s reference count, but if that loop object goes away, Promise::ES6 won’t work until you call use_event() again.

IMPORTANT: For the best long-term scalability and flexibility, your code should work with either synchronous or asynchronous promises.

CANCELLATION

Promises have never provided a standardized solution for cancellation—i.e., aborting an in-process operation. If you need this functionality, then, you’ll have to implement it yourself. Two ways of doing this are:

You’ll need to decide if it makes more sense for your application to leave a canceled query in the “pending” state or to “settle” (i.e., resolve or reject) it. All things being equal, I feel the first approach is the most intuitive, while the latter ends up being “cleaner”.

Of note: Future implements native cancellation.

MEMORY LEAKS

It’s easy to create inadvertent memory leaks using promises in Perl. Here are a few “pointers” (heh) to bear in mind:

SEE ALSO

If you’re not sure of what promises are, there are several good introductions to the topic. You might start with this one.

Promise::XS is my refactor of AnyEvent::XSPromises. It’s a lot like this library but implemented mostly in XS for speed.

Promises is another pure-Perl Promise implementation.

Future fills a role similar to that of promises. Much of the IO::Async ecosystem assumes (or strongly encourages) its use.

CPAN contains a number of other modules that implement promises. I think mine are the nicest :), but YMMV. Enjoy!

LICENSE & COPYRIGHT

Copyright 2019-2021 Gasper Software Consulting.

This library is licensed under the same terms as Perl itself.