then() — Chaining & Auto-Flatten
then() is the primary chaining mechanism for Promise<T>. It transforms a resolved value into a new Promise, with automatic flattening.
Basic chaining — .await()
xpp::resolve(10)
.then([](int x) { return x * 2; }) // Promise<int>
.then([](int x) { return x + 1; }) // Promise<int>
.then([](int x) { // Promise<void>
printf("result: %d\n", x);
});
// final chain: Promise<void>
Each .then() adds one link to the chain. The chain only starts running when the root promise (here resolve(10)) is polled by the event loop.
Auto-Flatten
If a .then() callback returns a Promise<U>, the result is flattened to Promise<U> — not Promise<Promise<U>>. This is the same behavior as Rust's Future::and_then and JavaScript's Promise.then.
Promise<User> fetch_user(int id) { /* ... */ }
Promise<Order> fetch_order(User &u) { /* ... */ }
Promise<Order> order = resolve(42)
.then([](int id) { return fetch_user(id); }) // → Promise<User> (flattened)
.then([](User u) { return fetch_order(u); }); // → Promise<Order> (flattened)
Without auto-flatten, the result would be Promise<Promise<User>> — requiring .then().then() to unwrap. With it, you write a flat chain.
Type transformations
Promise<std::string> msg = resolve(10)
.then([](int x) { return x * 2; }) // Promise<int>
.then([](int x) { return std::to_string(x); }); // Promise<std::string>
Each .then(fn) turns Promise<T> into Promise<decltype(fn(T))>. The compiler tracks types through the entire chain.
Void handling
Promise<> p = resolve(10)
.then([](int x) { printf("%d\n", x); });
// p is Promise<void>
then() is non-mutating
Each .then() call returns a new Promise — the original is untouched. This means you can fork a chain into multiple consumers:
auto root = fetch_value();
auto doubled = root.then([](int x) { return x * 2; });
auto tripled = root.then([](int x) { return x * 3; });
Error handling
There's no built-in catch method. Errors are propagated through the chain as regular values using Result<T, E>:
Promise<Result<int, MyError>> compute = resolve(42)
.then([](int x) -> Result<int, MyError> {
if (x == 0) return err(MyError::DivideByZero);
return ok(100 / x);
})
.then([](Result<int, MyError> r) {
return r.is_ok() ? r.unwrap() * 2 : 0;
});
Arenas: allocation model
Each .then() chain shares a 256-byte bump allocator (arena). Promise nodes are allocated from this arena, not individually heap-allocated. This means a 10-link chain is a single malloc (the arena) rather than 10 separate allocations. Nodes that overflow the arena fall back to heap transparently.
Driving the chain
Chains are lazy — nothing runs until polled. Use .await() to drive the chain to completion on the current thread:
int result = resolve(10)
.then([](int x) { return x * 2; })
.await();
// result == 20
then() vs .await() vs co_await
These three are equivalent:
// C++11: then() + .await()
Promise<int> p = fetch_value()
.then([](int x) { return x * 2; });
// C++11 + fiber: .await()
int val = xpp::fiber([]() {
int x = fetch_value().await();
return x * 2;
}).await();
// C++20: co_await
Promise<int> p = async_compute(); // inside: int x = co_await fetch_value(); co_return x * 2;
All produce the same Promise<int> backed by the same poll() mechanism.