Async & Coroutines

June 23, 2026 · View on GitHub

Aria's async layer provides C++20 coroutine-based primitives for asynchronous work, built on top of the reactive graph. The key types:

  • Task<T> — lazy, single-shot coroutine awaitable
  • AsyncCommand<R, Args...> — three-state async action (executing / error / result)
  • CoroutineScope / ViewModelScope — structured concurrency tied to a lifetime
  • CancellationToken — cooperative cancellation
  • Combinatorswhen_all, when_any, with_timeout

Include: #include "aria/async/task.hpp", #include "aria/async/async_command.hpp", etc.


Task<T>

Task<T> is a lazy coroutine — it does nothing until co_awaited.

Basic Usage

aria::async::Task<int> compute_value() {
    co_return 42;
}

aria::async::Task<void> show_result() {
    int val = co_await compute_value();
    std::cout << "Got: " << val << "\n";
}

Void Specialization

aria::async::Task<void> log_message(std::string msg) {
    std::cout << msg << "\n";
    co_return;
}

Exception Handling

Exceptions thrown inside the coroutine body are stored and re-thrown at the co_await site:

aria::async::Task<int> risky() {
    throw std::runtime_error("boom");
    co_return 0;  // unreachable
}

aria::async::Task<void> caller() {
    try {
        int v = co_await risky();
    } catch (const std::runtime_error& e) {
        // Caught here
    }
}

Fire-and-Forget (Detached)

aria::async::Task<void> background_work() {
    // Long-running work...
    co_return;
}

// Start without awaiting — runs independently
background_work().start_detached();

Warning: Detached tasks have no lifetime guard. Ensure captured references outlive the coroutine.


CancellationToken / CancellationSource

Cooperative cancellation. A CancellationSource owns the flag; a CancellationToken is a read-only view.

aria::async::CancellationSource src;
aria::async::CancellationToken tok = src.token();

tok.is_cancelled();   // false
src.cancel();
tok.is_cancelled();   // true

Inside a Coroutine

aria::async::Task<void> poll_loop(aria::async::CancellationToken tok) {
    while (!tok.is_cancelled()) {
        co_await aria::async::sleep_for(100ms);
        tok.throw_if_cancelled();  // throws OperationCancelled
        do_work();
    }
}

Executor

Executors abstract scheduling — where coroutines run.

#include "aria/async/executor.hpp"

// Typically provided by the platform adapter
aria::async::IExecutor& ui;      // main/UI thread
aria::async::IExecutor& worker;  // background thread pool

Schedule On

Hop between executors:

aria::async::Task<UserProfile> load_profile(int uid) {
    co_await aria::async::schedule_on(worker);  // jump to background
    auto data = fetch_from_db(uid);              // blocking OK here
    co_await aria::async::schedule_on(ui);       // hop back to UI
    co_return data;
}

AsyncCommand<R, Args...>

An async command exposes three reactive properties that the UI can bind to:

PropertyTypeMeaning
is_executingProperty<bool>True while any invocation is in flight
last_errorProperty<std::optional<Error>>Most recent error, nullopt when OK
last_resultProperty<std::optional<R>>Most recent successful result (R ≠ void)

Basic Usage

#include "aria/async/async_command.hpp"

aria::async::AsyncCommand<SearchResult, std::string> search{ui, worker,
    [](std::string query) -> aria::async::Task<SearchResult> {
        co_await aria::async::schedule_on(worker);
        co_return perform_search(query);
    }
};

// Trigger from UI
search.execute("hello");

// Bind in UI
search.is_executing.bind([](bool running) {
    spinner.set_visible(running);
});

Cancellable Action

Accept a CancellationToken as the first parameter:

aria::async::AsyncCommand<Data, int> fetch{ui, worker,
    [](aria::async::CancellationToken tok, int id) -> aria::async::Task<Data> {
        co_await aria::async::schedule_on(worker);
        tok.throw_if_cancelled();
        co_return heavy_load(id);
    }
};

Concurrency Policies

#include "aria/async/async_command.hpp"

// Parallel (default): multiple invocations run concurrently
aria::async::AsyncCommand<void, std::string> cmd_parallel{
    ui, worker, action, {}, aria::async::AsyncCommandPolicy::Parallel};

// LatestOnly: new execute() cancels in-flight work (search-as-you-type)
aria::async::AsyncCommand<Results, std::string> cmd_latest{
    ui, worker, action, {}, aria::async::AsyncCommandPolicy::LatestOnly};

// DropIfRunning: ignore execute() while busy (prevent double-submit)
aria::async::AsyncCommand<void> cmd_drop{
    ui, worker, action, {}, aria::async::AsyncCommandPolicy::DropIfRunning};

Inside a ViewModel

class SearchVm : public aria::binding::ViewModel {
public:
    SearchVm(aria::async::IExecutor& ui, aria::async::IExecutor& worker)
        : search(ui, worker,
            [this](std::string q) -> aria::async::Task<Result> {
                co_await aria::async::schedule_on(worker);
                co_return do_search(q);
            })
    {
        scope_.attach(*this);
    }

    aria::Property<std::string> query{""};
    aria::async::AsyncCommand<Result, std::string> search;

private:
    aria::binding::ViewModelScope scope_;
};

ViewModelScope

Ties CoroutineScope to a ViewModel's lifetime. Destroying the VM cancels and joins all in-flight coroutines.

#include "aria/binding/view_model_scope.hpp"

class PollingVm : public aria::binding::ViewModel {
public:
    PollingVm() { scope_.attach(*this); }

    void start() {
        scope_.launch([this](aria::async::CancellationToken tok) -> aria::async::Task<void> {
            while (!tok.is_cancelled()) {
                co_await aria::async::sleep_for(1s);
                tok.throw_if_cancelled();
                refresh();
            }
        });
    }

private:
    aria::binding::ViewModelScope scope_;
};

When PollingVm is destroyed, scope_ calls cancel_and_join() (default 5 s timeout). Stuck coroutines are reported as leaks.


Combinators

when_all

Wait for all tasks to complete:

aria::async::Task<void> load_all() {
    auto [users, posts, comments] = co_await aria::async::when_all(
        load_users(),
        load_posts(),
        load_comments()
    );
}

when_any

Complete when the first task finishes (others are cancelled):

aria::async::Task<Response> race_servers() {
    co_return co_await aria::async::when_any(
        fetch_from_primary(),
        fetch_from_backup()
    );
}

with_timeout

Abort if a task exceeds the deadline:

aria::async::Task<Data> fetch_with_deadline() {
    co_return co_await aria::async::with_timeout(
        slow_fetch(),
        std::chrono::seconds(5)
    );
}

Quick Reference

TypePurposeProduces Value
Task<T>Lazy coroutineYes (T)
AsyncCommand<R, Args...>Three-state async actionVia last_result
CoroutineScopeLaunch + cancel coroutinesNo
ViewModelScopeScope tied to VM lifetimeNo
CancellationTokenCooperative cancellation checkNo
CancellationSourceCancel producerNo
IExecutorWhere to runNo

See Also