NAME

Acme::Parataxis::Nursery - Structured Concurrency Container for Spawned Children that are all Joined, or all Cancelled

SYNOPSIS

use v5.40;
use Acme::Parataxis qw[async nursery];

async {
    my ($profile, $orders) = (undef, undef);

    my $ok = eval {
        nursery( sub ($n) {
            $n->spawn( sub { $profile = fetch_user_profile() } );
            $n->spawn( sub { $orders  = fetch_user_orders()  } );
        } );
        1;
    };
    die $@ unless $ok;

    say $profile->name;    # both children finished before the nursery returned
};

DESCRIPTION

Acme::Parataxis::Nursery is the runtime behind nursery( ) (exported by Acme::Parataxis). It gives you the Go-structured-concurrency pattern in Perl:

  • Any child you spawn with $n->spawn is guaranteed to be finished before nursery( ) returns.

  • The first child to fail cancels all of its siblings. The whole group is drained, then the failure is rethrown in the calling fiber as an Acme::Parataxis::Error::Nursery aggregate.

  • The block value comes back when every child succeeded.

A bare fiber/spawn inside the block creates an independent fiber the block owns - it is not adopted by the nursery and does not delay the join. The Acme::Parataxis::CancellationToken that the nursery hands you via $n->token is public, so you can cancel the whole group yourself, or hand it to a with_timeout deadline.

CONSTRUCTOR

new( [ token => $token ] )

my $n = Acme::Parataxis::Nursery->new( token => $tok );

Normally you never call this: nursery( ) builds the object, runs your block in the calling fiber, joins every child, and throws the aggregate. new is the lower-level entry point for embedding, and accepts a token (default: a fresh Acme::Parataxis::CancellationToken). Note that new alone does not join - a child you spawn through it still runs, but nothing aggregates its error or cancels its siblings; that is the private _join that nursery( ) calls on your behalf.

METHODS

spawn( sub { ... } )

$n->spawn( sub { ... } );

Register a child fiber for this nursery and return the child fiber object. The child runs on its own fiber; the nursery guarantees it is reaped before nursery( ) returns.

The child parks once at birth, so a failure in its very first statement surfaces through the scheduler (where the nursery can observe and aggregate it) rather than being thrown into your block.

Croaks with Nursery::spawn() requires a CODE ref if the argument is not a code ref, and with Nursery::spawn() must be called from inside a scheduled fiber if you are not inside a fiber.

token( )

my $tok = $n->token;

The nursery's public cancellation token. Cancelling it cancels and drains every child, exactly as if a sibling had failed. Passing it to a with_timeout deadline lets the timeout cancel the group as well. A clean nursery leaves the token uncancelled.

Recipes

Fan out and collect

my (@rows, $err);

eval {
    nursery( sub ($n) {
        for my $id (@ids) {
            $n->spawn( sub { push @rows, load_row($id) } );
        }
    } );
    1;
} or $err = $@;

die $err if $err;

Bound the group with a deadline

async {
    eval {
        nursery( sub ($n) {
            $n->spawn( sub { await_sleep(5_000) } );
            $n->spawn( sub { await_sleep(5_000) } );
            with_timeout( 500, $n->token, sub { await_sleep(10_000) } );
        } );
        1;
    } or warn "the group was cut short: $@";
};

The deadline cancels the nursery's own token, which cancels both children.

ERRORS

Acme::Parataxis::Error::Nursery

Thrown by nursery( ) when one or more children died. It is an Acme::Parataxis::Error subclass, so $@->message, $@->kind (nursery), and the usual accessors work. Stringifying it gives nursery failure: ... built from the primary failure.

$err->failures returns every child error in spawn order, including the Acme::Parataxis::Error::Cancelled objects that the cancelled siblings threw as they unwound. $err->primary is the first natural (non-cancelled) failure, so it points at the error that actually broke something rather than at a consequence:

my $err = eval { nursery( sub ($n) {
    $n->spawn( sub { die "real failure\n" } );
    $n->spawn( sub { await_sleep(5_000) } );
} ); 1 } ? undef : $@;

$err->primary;                 # "real failure\n"
my @all = $err->failures;      # 2 - the failure plus the cancelled sibling

Behavior Notes

A child sets a lexical; returning from the block does not deliver a value

The block cannot return early to hand a child its result: the runtime joins every spawned child, so a child's result must be expressed by setting a lexically-scoped variable, as in the SYNOPSIS. The block's own return value is the value of nursery( ), and it is only delivered when every child succeeded.

A child's death cancels its siblings, so most siblings never finish

The first child to die cancels the rest. A sibling that had not yet completed by then throws Acme::Parataxis::Error::Cancelled instead of its own result, and the group is drained before the aggregate is rethrown. In other words, a nursery is all-or-nothing, not best-effort: on failure you learn about the first real error plus the cancellations it caused, not the output of the children that were still in flight.

The block's own death wins over its children's

If your block dies, the runtime cancels and drains the children so none are orphaned, then rethrows your error verbatim - not wrapped in an Error::Nursery. Only child failures produce the aggregate.

Nested nurseries are independent

An inner nursery( ) that fails cancels only its own children. Catch it (as in the outer-failure recipe) and the outer block and its siblings carry on.

SEE ALSO

Acme::Parataxis, Acme::Parataxis::CancellationToken, Acme::Parataxis::Error, Acme::Parataxis::Future

AUTHOR

Sanko Robinson https://github.com/sanko

LICENSE

Copyright (C) 2026 Sanko Robinson.

This library is free software; you can redistribute it and/or modify it under the terms found in the Artistic License 2.