Promise...tleIntro(3)

πŸ“¦
version 1.05 Β· 2 versions
, in fedora 43

✨ Promises::Cookbook::GentleIntro ✨

okay bestie, this is the Promises::Cookbook::GentleIntro(3) page β€” User Contributed Perl Documentation, and it is literally all you need to know about Promises. 🫢

✨ NAME ✨

Promises::Cookbook::GentleIntro β€” All you need to know about Promises

🌸 VERSION 🌸

version 1.05

πŸ’… All you need to know about Promises πŸ’…

If you have ever done any async programming, you will be familiar with "callback hell", where one callback calls another, calls another, calls another... 😭 Promises give us back a top-to-bottom coding style, making async code easier to manage and understand. It looks like synchronous code, but execution is asynchronous. periodt.

The Promises module is event loop agnostic β€” it can be used with any event loop. Backends exist for AnyEvent (and thus all the event loops supported by AnyEvent) and Mojo::IOLoop. But more of this later in "Integration with event loops". ✨

There are two moving parts:

  • Deferred objects β€” Deferred objects provide the interface to a specific async request. They execute some asynchronous action and return a promise.
  • Promise objects β€” A promise is like a placeholder for a future result. The promise will either be resolved in case of success, or rejected in case of failure. Promises can be chained together, and each step in the chain is executed sequentially.

The easiest way to understand how Deferred and Promise objects work is by example. πŸ‘‘

πŸ¦‹ Deferred objects πŸ¦‹

A deferred object is used to signal the success or failure of some async action which can be implemented in the async library of your choice. For instance:

use Promises qw(deferred);
use AnyEvent::HTTP qw(http_get);
use JSON qw(decode_json);

sub fetch_it {
    my ($uri) = @_;
    my $deferred = deferred;
    http_get $uri => sub {
        my ($body, $headers) = @_;
        $headers->{Status} == 200
            ? $deferred->resolve( decode_json($body) )
            : $deferred->reject( $headers->{Reason} )
    };
    $deferred->promise;
}

The above code makes an asynchronous http_get request to the specified $uri. The result of the request at the time the subroutine returns is like SchrΓΆdinger's cat: both dead and alive. In the future it may succeed or it may fail. iykyk.

This sub creates a Promises::Deferred object using deferred, which is either:

  • resolved on success, in which case it returns the request "body", or
  • rejected on failure, in which case it returns the reason for failure.

As a final step, the deferred object returns a Promises::Promise object which represents the future result.

That's all there is to know about Promises::Deferred. πŸ’–

✨ Promise objects ✨

Promises are a lot like "try"/"catch"/"finally" blocks except that they can be chained together. The most important part of a promise is the then() method:

$promise->then(
    sub { success! },
    sub { failure }
);

The then() method takes two arguments: a success callback and a failure callback. But the important part is that it returns a new promise, which is the thing that allows promises to be chained together. slay.

The simple genius of promises (and I can say that because I didn't invent them) will not be immediately obvious, but bear with me. Promises are very simple, as long as you understand the execution flow:

πŸ’– Resolving or rejecting a Promise πŸ’–

use Promises qw(deferred);

my $deferred = deferred;
$deferred->promise->then(
    sub { say "OK! We received: ".shift(@_)},       # on resolve
    sub { say "Bah! We failed with: ". shift(@_)}   # on reject
);

What this code does depends on what happens to the $deferred object:

$deferred->resolve('Yay!');
# prints: "OK! We received: Yay!"

$deferred->reject('Pooh!');
# prints "Bah! We failed with: Pooh!"

A Deferred object can only be resolved or rejected once. Once it is resolved or rejected, it informs all its promises of the outcome. no cap.

πŸ”— Chaining resolve callbacks πŸ”—

As mentioned earlier, the then() method returns a new promise which will be resolved or rejected in turn. Each "resolve" callback will receive the return value of the previous "resolve" callback:

deferred
->resolve('red','green')
->promise

->then(sub {
    # @_ contains ('red','green')
    return ('foo','bar');
})

->then(sub {
    # @_ contains ('foo,bar');
    return 10;
})

->then( sub {
    # @_ contains (10)
});

All of these example callbacks have just returned a simple value (or values), so execution has moved from one callback to the next.

🫢 Chaining reject callbacks 🫢

Note that in the above example, in each call to then() we specified only a resolved callback, not a rejected callback. If a promise is resolved or rejected, the action gets passed down the chain until it finds a resolved or rejected handler. This means that errors can be handled in the appropriate place in the chain:

my $deferred = deferred;

$deferred->promise
->then(
    sub {
        my $count = shift();
        say "Count: $count";
        return $count+1;
    }
)
->then(
    sub {
        my $count = shift();
        say "Count: $count";
        return $count+1;
    }
)->then(
    sub {
        my $count = shift();
        say "Final count: $count";
        return $count+1;
    },
    sub {
        my $reason = shift;
        warn "Failed to count: $reason"
    }
);

If the $deferred object is resolved, it will call each resolved callback in turn:

$deferred->resolve(5);
# prints:
#   Count: 5
#   Count: 6
#   Final count: 7

If the $deferred object is rejected, however, it will skip all of the steps in the chain until it hits the first rejected callback:

$deferred->reject('Poor example');
# warns:
#    "Failed to count: Poor example"
Important: Event loops do not like fatal exceptions! For this reason the resolved and rejected callbacks are run in eval blocks. Exceptions thrown in either type of callback are passed down the chain to the next rejected handler. If there are no more rejected handlers, then the error is silently swallowed. 😭

⚑ Throwing and handling exceptions ⚑

While you can signal success or failure by calling resolve() or reject() on the $deferred object, you can also signal success or failure in each step of the promises chain.

  • Resolved callbacks are like "try" blocks: they can either execute some code successfully or throw an exception.
  • Rejected callbacks are like "catch" blocks: they can either handle the exception or rethrow it.
$deferred = deferred;

$deferred->promise
->then(
    sub {
        my $count = shift;
        die "Count too high!" if $count > 100;
        return $count
    }
)->then(
    sub {
        say "The count is OK. Continuing";
        return @_
    },
    sub {
        my $error = shift;
        warn "We have a problem: $error";
        die $error;
    }
)->then(
    undef,  # no resolved handler
    sub { return 1; }
)-> then(
    sub {
        my $count = shift;
        say "Got count: $count";
    }
)

There are a few ways this code can execute. We can resolve the $deferred object with a reasonable count:

$deferred->resolve(5);
# prints:
#   The count is OK. Continuing
#   Got count: 5

$defer

If we reject the $deferred object, the first rejected handler is called. It warns, then rethrows the exception with "die" which calls the next rejected handler. This handler resolves the exception (that is, it doesn't call "die") and returns a value which gets passed to the next resolved handler:

$deferred->reject('For example purposes')
# warns:
#    We have a problem: For example purposes
# prints:
#    Got count: 1

Finally, if we resolve the $deferred object with a too large count, the first resolved handler throws an exception, which calls the next rejected handler:

$deferred->resolve(1000);
# warns:
#    We have a problem: Count too high!
# prints:
#    Got count: 1

πŸŽ€ catch() πŸŽ€

In the above example, we called then() with "undef" instead of a resolved callback. This could be rewritten to look a bit cleaner using the catch() method, which takes just a rejected callback.

# these two lines are equivalent:
$promise->then( undef, sub { rejected cb} )
$promise->catch( sub { rejected cb } )

🌸 finally() 🌸

Any "try"/"catch" implementation has a "finally" block, which can be used to clean up resources regardless of whether the code in the "try" block succeeded or failed. Promises offer this functionality too. iconic.

The finally() method accepts a single callback which is called regardless of whether the previous step was resolved or rejected. The return value (or any exception thrown in the callback) are thrown away, and the chain continues as if it were not there:

$deferred = deferred;
$deferred->promise
->then(
    sub {
        my $count = shift;
        if ($count > 10) { die "Count too high"}
        return $count
    }
)->finally(
    sub { say "Finally got: ".shift(@_) }
)->then(
    sub { say "OK: ". shift(@_)   },
    sub { say "Bah!: ". shift(@_) }
);

If we resolve the $deferred object with a good count, we see:

$d->resolve(5);
# prints:
#   Finally got: 5
#   OK: 5

With a high count we get:

$d->resolve(20);
# prints:
#   Finally got: Count to high
#   Bah: 20

🌈 Chaining async callbacks 🌈

This is where the magic starts: each resolved/rejected handler can not only return a value (or values), it can also return a new Promise. Remember that a Promise represents a future value, which means that execution of the chain will stop until the new Promise has been either resolved or rejected!

For instance, we could write the following code using the fetch_it() sub (see "Deferred objects") which returns a promise:

fetch_it('http://domain.com/user/123')
->then(
    sub {
        my $user = shift;
        say "User name: ".$user->{name};
        say "Fetching total comments";
        return fetch_id($user->{total_comments_url});
    }
)->then(
    sub {
        my $total = shift;
        say "User has left $total comments"
    }
)
->catch(
    sub {
        warn @_
    }
);

okay so this code is literally sending an async request to grab the page for user 123 and it hands you back a promise. The second that promise resolves, it fires off another async request to get the total comments for that user, and guess what? another promise. Once that second promise resolves, it prints the total number of comments. And if either promise gets rejected? it skips down the chain looking for the first rejected handler and runs that one. periodt. πŸ’…

The whole vibe is that it's organised to look like synchronous code. Each step happens sequentially, it's easy to read, easy to understand, but bestie it works asynchronously. While we're waiting for a response from domain.com (so while our promise is still unfulfilled), the event loop can happily keep running code elsewhere in the application. no cap. ✨

And actually? it's not just Promises::Promise objects that can be returned β€” it can be any object that's "thenable" (aka it has a then() method). So if you wanna integrate your Promises code with a library that's using Future objects, you should be able to do it. slay. πŸ¦‹

✨ Running async requests in parallel ✨

Sometimes order literally doesn't matter: maybe we wanna retrieve several web pages at the same time. For that, we can use the collect helper:

use Promises qw(collect);

collect(
    fetch_it('http://rest.api.example.com/-/product/12345'),
    fetch_it('http://rest.api.example.com/-/product/suggestions?for_sku=12345'),
    fetch_it('http://rest.api.example.com/-/product/reviews?for_sku=12345'),
)->then(
    sub {
        my ($product, $suggestions, $reviews) = @_;
        # do something with these values
    },
    sub { warn @_ }
);

collect() takes a list of promises and returns a new promise (we'll call it $p for clarity). When all of its promises have been resolved, it resolves $p with the values returned by every promise, in the same order they were passed into collect(). πŸ’–

Note: each promise can return multiple values, so $product, $suggestions, and $reviews in the example above will all be array refs. iykyk. 🌸

If any of the passed-in promises is rejected, then $p will also be rejected with the reason for the failure. $p can only be rejected once, so we'll only find out about the first failure. πŸ’€

πŸŒ€ Integration with event loops πŸŒ€

To run asynchronous code, you need to run some event loop. That can be as simple as using "CONDITION VARIABLES" in AnyEvent to run the event loop just until a particular condition is met:

use AnyEvent;

my $cv = AnyEvent->condvar;
collect(
    fetch_it('http://rest.api.example.com/-/product/12345'),
    fetch_it('http://rest.api.example.com/-/product/suggestions?for_sku=12345'),
    fetch_it('http://rest.api.example.com/-/product/reviews?for_sku=12345'),
)->then(
    sub {
        my ($product, $suggestions, $reviews) = @_;
        $cv->send({
            product     => $product->[0],
            suggestions => $suggestions->[0],
            reviews     => $reviews->[0],
        })
    },
    sub { $cv->croak( 'ERROR' ) }
);

# wait for $cv->send or $cv->croak
my $results = $cv->recv;

More usually though, a whole application is meant to be asynchronous, and in that case the event loop just runs continuously. Normally you'd only need to use $cv's or the equivalent at the point where your application uses a specific async library, as explained in "Deferred objects". The rest of your code can deal purely with Promises. πŸ’…

βš™οΈ Event loop specific backends βš™οΈ

The resolved and rejected callbacks should be run by the event loop, rather than having one callback call the next, which calls the next, etc. 🫢

In other words, if a promise is resolved, it doesn't call the resolved callback directly. Instead it adds it to the event loop's queue, then returns immediately. The next time the event loop checks its queue, it'll find the callback in the queue and will call it. periodt. πŸ”₯

By default, Promises is event loop agnostic, which means it doesn't know which event loop to use, so each callback ends up calling the next, etc. If you're writing Promises-based modules for CPAN, then your code should also be event loop agnostic, in which case you want to use Promises like this:

use Promises qw(deferred collect);

However, if you're an end user, then you should specify which event loop you're using at the start of your application:

use Promises backend => ['AnyEvent']; # or "EV" or "Mojo"

You only need to specify the backend once β€” any code in the application that uses Promises will automatically use the specified backend. 🫢

🧚 Recursing safely with with done() 🧚

One of the cool things about working with promises is that the return value gets passed down the chain as if the code were synchronous. However, that's not always what we want. 😭

Imagine we wanna process every line in a file, which could be millions of lines. We don't care about the results from each line β€” all we care about is whether the whole file was processed successfully, or whether something failed.

In sync code we'd write something like this:

sub process_file {
    my $fh = shift;
    while (my $line = <$fh>) {
        process_line($line)
            || die "Failed"
    }
}

Now imagine that process_line() runs asynchronously and returns a promise. By the time it returns, it probably hasn't executed anything yet. We can't just go ahead and read the next line of the file, otherwise we could generate a billion promises before any of them has had time to execute. πŸ’€

Instead, we need to wait for process_line() to complete and only then move on to reading the next line. We could do this as follows:

# WARNING: EXAMPLE OF INCORRECT CODE #

use Promises qw(deferred);

sub process_file {
    my $fh        = shift;
    my $deferred  = deferred;
    my $processor = sub {
        my $line = <$fh>;
        unless (defined $line) {
            # we're done
            return $deferred->resolve;
        }
        process_line($line)->then(

            # on success, call $processor again
            __SUB__,

            # on failure:
            sub {
                return $deferred->reject("Failed")
            }
        )
    }

    # start the loop
    $processor->();

    return $deferred->promise
}

This code has two stack problems. The first is that every time we process a line, we recurse into the current __SUB__ from the current sub. This problem is solved by specifying one of the "Event loop specific backends" somewhere in our application, which we discussed above. πŸ™ˆ

The second problem is that every time we recurse into the current __SUB__ we're waiting for the return value. Other languages use the Tail Call optimization <http://en.wikipedia.org/wiki/Tail_call> to keep the return stack flat, but we don't have this option. 😭

Instead, we have the done() method which, like then(), accepts a resolved callback and a rejected callback. But it differs from then() in two ways:

  • It doesn't return a promise, which means the chain ends with the done() step. periodt. πŸ’…
  • Callbacks are not run in an eval block, so calling die() will throw a fatal exception. (Most event loops, however, will catch the exception, warn, and continue running.) 🚨

The code can be rewritten using done() instead of then() and an event loop specific backend, and it will happily process millions of lines without memory leaks or stack oveflows:

use Promises backend => ['EV'], 'deferred';

sub process_file {
    my $fh        = shift;
    my $deferred  = deferred;
    my $processor = sub {
        my $line = <$fh>;
        unless (defined $line) {
            # we're done
            return $deferred->resolve;
        }
        #### USE done() TO END THE CHAIN ####
        process_line($line)->done(

            # on success, call $processor again
            __SUB__,

            # on failure:
            sub {
                return $deferred->reject("Failed")
            }
        )
    }

    # start the loop
    $processor->();

    return $deferred->promise
}

πŸ‘‘ AUTHOR πŸ‘‘

Stevan Little <stevan.little@iinteractive.com>

This software is copyright (c) 2025, 2017, 2014, 2012 by Infinity Interactive, Inc.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.


πŸ’– That's a wrap, bestie! This page is from perl v5.42.0, dated 2025-07-25, and it's Promise...tleIntro(3). Stay iconic, stay slay! πŸ’–

πŸ“¦ where this page comes from