Utilities: try_next
When to Use
You need to try multiple items with an async function, returning the first success — like falling through a list of DNS addresses until one connects.
xpp::try_next(items, fn)
Calls fn(item) on each item in items sequentially. Returns the first ok result, or the last err if all fail.
- C++11-compatible: uses
struct + std::move(*this)chaining, no coroutines or concepts - Zero heap allocation in the combinator itself (items and fn stored by value)
- Duck-typed:
fn(item)must returnPromise<Result<T, E>>whereResulthas.is_ok() - Factory function:
try_next(items, fn)returns a callable; invoke with()to start
#include <xpp/promise_utils.h>
// Try each resolved DNS address until one connects
std::vector<SocketAddr> addrs = co_await resolve_host("example.com");
auto stream = xpp::try_next(std::move(addrs), [&](const SocketAddr &a) {
return TcpStream::connect_with_conf(a.ip().c_str(), a.port(), conf.get());
})().await();
// Returns first ok, or last error (ConnectionRefused from final address)
Immediate results
When fn returns immediately-resolved promises, try_next evaluates without touching the event loop:
std::vector<int> items = {10, 20, 30};
auto result = xpp::try_next(std::move(items), [](int x) -> Promise<Result<int, int>> {
if (x == 20) return xpp::resolve(Result<int, int>(ok, x * 10));
return xpp::resolve(Result<int, int>(err, -x));
})().await();
// result == 200 (20 × 10), only tried 10 (failed) and 20 (ok)
Fall-through chain
When all items fail, returns the last error:
std::vector<int> items = {1, 2, 3};
auto err = xpp::try_next(std::move(items), [](int x) -> Promise<Result<int, int>> {
return xpp::resolve(Result<int, int>(err, x));
})().await();
// err == 3, all three were tried
Deferred (async) results
Works with async operations scheduled on the event loop:
std::vector<int> items = {10, 20, 30};
auto ar1 = xpp::async<Result<int, int>>();
auto ar2 = xpp::async<Result<int, int>>();
// r1 resolves with error at 10ms, r2 resolves with success at 20ms
schedule_resolve(ar1.second, Result<int, int>(err, -1), 10);
schedule_resolve(ar2.second, Result<int, int>(ok, 99), 20);
auto result = xpp::try_next(std::move(items), [&, p1 = std::move(ar1.first),
p2 = std::move(ar2.first)]
(int) mutable -> Promise<Result<int, int>> {
static int call_count = 0;
call_count++;
if (call_count == 1) return std::move(p1);
return std::move(p2);
})().await();
// result == 99 (second item succeeded after the first failed)
How It Works
TryNext<Items, Func> is a callable struct. Calling operator() pushes the first item through fn(), then chains a .then() callback:
// Simplified:
struct TryNext {
Items items;
size_t idx;
Func fn;
P operator()() {
return fn(items[idx++]).then(Then{std::move(*this)});
}
struct Then {
TryNext next; // ownership transferred via move
template <class R>
P operator()(R &&r) {
if (r.is_ok()) return xpp::resolve(std::forward<R>(r));
if (next.idx >= next.items.size())
return xpp::resolve(std::forward<R>(r)); // last error
return next(); // try next item
}
};
};
Key design points:
std::move(*this)ownership transfer: theTryNextstruct (withitemsandfnby value) moves through each.then()node in the Promise chain — noshared_ptrrefcount overhead- Template
operator()inThen: acceptsResult<T, E>without spelling out the types, enabling duck-typing of anyResulttype with.is_ok() - Tail-recursive via Promise chain:
return next()creates a new.then()link, rather than growing the call stack
Performance
| Allocation | Count |
|---|---|
| TryNext struct (items + fn) | 0 (stack/inline) |
| PromiseNode chain | 1 heap (head) + N arena bumps → 1 bulk free |
The combinator itself is zero-allocation. The Promise chain uses xpp's per-chain arena allocator, so only the head node hits the heap — subsequent .then() nodes are bump-allocated in the arena.
Why Not try_each?
The original name was try_each, but that misleadingly implies all items are always tried. The actual semantics are "try one, fail → try the next, succeed → stop". try_next captures this accurately: it parallels try_next() / next() iteration patterns in Rust's Iterator trait.