Promise...ecursion(3)

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

πŸ’– NAME πŸ’–

Promises::Cookbook::Recursion β€” the whole vibe is examples of recursive asynchronous operations, bestie. That's it, that's the girl. πŸ¦‹


⭐ VERSION ⭐

version 1.05 β€” yes, she's serving 1.05, no cap. πŸ’…


🌸 SYNOPSIS 🌸

okay so here's the whole recipe, exactly as written, don't touch a single character:

package MyClass;

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

sub new                 {...}
sub process             {...}
sub is_finished         {...}
sub fetch_next_from_db  {...} # returns a promise

sub fetch_all {
    my $self     = shift;
    my $deferred = deferred;

    $self->_fetch_loop($deferred);
    return $deferred->promise;
}

sub _fetch_loop {
    my ($self,$deferred) = @_;
    if ( $self->is_finished ) {
        $deferred->resolve;
        return
    }
    $self->fetch_next_from_db
         ->then( sub { $self->process(@_) })
         ->done(
            sub { $self->_fetch_loop($deferred) }
            sub { $deferred->reject(@_) }
         );
}

package main;

my $cv  = AnyEvent->condvar;
my $obj = MyClass->new(...);
$obj->fetch_all->then(
    sub { $cv->send(@_)          },
    sub { $cv->croak('ERROR',@_) }
);

$cv->recv;

like... it's giving recursion. it's giving async. it's giving slay. ✨


πŸŽ€ DESCRIPTION πŸŽ€

okay bestie, sit down, because this one is actually so good.

So collect() lets you wait around for a bunch of promises that are all running in parallel. super cute. BUT 😭 sometimes you don't want parallel β€” you need each step to happen in order, and the way you do that is by using promises recursively. Like:

  1. Fetch next page of results
  2. Process page of results
  3. If there are no more results, return success
  4. Otherwise, goto step 1

iconic loop, honestly. πŸ”₯

but. and this is the important part, do not skip this, it is not a vibe, it is a warning: recursion can result in very deep stacks and out of memory conditions. πŸ’€ that's the danger. That's the thing that will ruin your whole afternoon.

So there are two important steps for dealing with recursion effectively. Two. Not one. Two.

πŸ’œ Step one: use one of the event-loop backends

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

Here's why, fr fr: the default Promises::Deferred implementation calls the then() callbacks synchronously. The event-loop backends, on the other hand, call the callbacks asynchronously, in the context of the event loop. different energy entirely. ✨

πŸ’œ Step two: break the chain

okay so even with that, there's still a problem, and it's kind of lowkey tragic:

each "promise" passes its return value on to the next "promise" etc, so you still end up using a lot of memory with recursion. πŸ™ˆ We can avoid this by breaking the chain.

In our example, all we care about is whether all the steps in our process completed successfully or not. Each execution of steps 1 to 4 is independent. Step 1 does not need to receive the return value from step 4. periodt. πŸ’…

So we break the chain by using done() instead of then(). And here's the tea:

  • then() returns a new "promise" to continue the chain.
  • done() will execute either the success callback or the error callback and return an empty list β€” breaking the chain and rolling back the stack. πŸ‘‘

πŸ¦‹ walking through the SYNOPSIS code, line by line, because I love you πŸ¦‹

sub fetch_all {
    my $self     = shift;
    my $deferred = deferred;

    $self->_fetch_loop($deferred);
    return $deferred->promise;
}

The $deferred variable (and the promise that we return to the caller) will either be resolved once all results have been fetched and processed by the _fetch_loop(), or rejected if an error occurs at any stage of execution. either way she's doing her job. πŸ’–

sub _fetch_loop {
    my ($self,$deferred) = @_;

    if ( $self->is_finished ) {
        $deferred->resolve;
        return;
    }

If "is_finished" returns a true value (eg there are no more results to fetch), then we can resolve our promise, indicating success, and exit the loop. bye bestie, we're done here. πŸ‘‹βœ¨

    $self->fetch_next_from_db
         ->then( sub { $self->process(@_) })
         ->done(
            sub { $self->_fetch_loop($deferred) }
            sub { $deferred->reject(@_) }
         );
}

Otherwise we fetch the next page of results asynchronously from the DB and process them. If either of these steps (fetching or processing) fails, then we signal failure by rejecting our deferred promise and exiting the loop. If there is no failure, we recurse back into our loop by calling _fetch_loop() again. πŸ”

However, this recursion happens asynchronously. And this is the part that makes it all click, so pay attention:

What this code actually does is to schedule the call to _fetch_loop() in the next tick of the event loop.

And because we used done() instead of then(), we don't wait around for the return result β€” instead we return immediately, exiting the current execution, discarding the return results and rolling back the stack. 🧚 no stack growth, no memory explosion, just vibes and efficiency.

like... the way that this is just correct? obsessed. 🫢


🌈 AUTHOR 🌈

Stevan Little <stevan.little@iinteractive.com> β€” king behavior, honestly. πŸ‘‘


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.


and that's the whole page, bestie!! πŸŽ€ this was Promise...ecursion(3) from the User Contributed Perl Documentation, lovingly served up by perl v5.42.0 on 2025-07-25. go forth and recurse responsibly (asynchronously, with done(), not then() β€” you know the rules). mwah πŸ’–βœ¨πŸ¦‹

πŸ“¦ where this page comes from