Skip to content

Promises and Async

Oleksandr Geronime edited this page Jun 27, 2026 · 2 revisions

Promises and Async

Overview

SERP provides a family of async primitives for managing deferred results and continuation chains:

Primitive Purpose
serp::Promise<T> Single-shot async result — resolved or rejected once.
serp::Response<T> Result carrier passed to method handlers; the handler side of a Promise.
serp::Async<T> Helper wrapper for constructing async operations ergonomically.
serp::PPromise<T> Persistent promise — may be resolved multiple times (stream-like).

These primitives integrate with serp::Call and serp::Timer to express async workflows without callbacks-in-callbacks.


serp::Promise<T>

serp::Promise<T> represents a value of type T that will be available in the future.

Resolving and Rejecting

serp::Promise<int> promise;

// Success:
promise.resolve(42);

// Failure:
promise.reject(serp::Error("computation failed"));

A promise may be resolved or rejected exactly once. Subsequent calls are ignored.

Consuming with then()

.then() registers a continuation lambda that runs when the promise resolves:

promise.then([](int value) {
    use(value);
});

The continuation runs on the EventLoop of the service that registered it — not the EventLoop of the resolver. This is safe by design.

Chaining

.then() returns a new Promise for the return value of the lambda, enabling sequential async steps:

fetchData()
    .then([](RawData raw) {
        return parse(raw);            // returns Promise<ParsedData>
    })
    .then([this](ParsedData data) {
        return store(data);           // returns Promise<void>
    })
    .then([this]() {
        logInfo()->info("Done");
    });

Each step begins only after the previous resolves. Rejection short-circuits the chain — downstream .then() lambdas are skipped.


serp::Response<T>

serp::Response<T> is the handler side of an async method call. It is passed as the last argument to a serp::Call handler when the call has a non-void return type.

The handler captures Response and resolves it when the result is ready — possibly after the handler function has returned:

call.setHandler([this](std::string input, serp::Response<std::string> response) {
    // Kick off async work; capture response by value
    processAsync(input, [response](std::string result) mutable {
        response.resolve(result);
    });
    // Handler returns immediately; response is resolved later
});
Method Description
response.resolve(value) Deliver a successful result to the caller.
response.reject(error) Signal failure to the caller.

Response is move-only and safe to capture by value in lambdas. It may be passed across threads. The framework ensures delivery on the caller's EventLoop.


serp::Async<T>

serp::Async<T> is a convenience wrapper that packages a Promise and its resolution logic together. It is typically used to bridge callback-based APIs into the promise model:

serp::Async<int> async([](serp::Promise<int> promise) {
    startBackgroundJob([promise]() mutable {
        promise.resolve(computeResult());
    });
});

async.then([](int result) {
    use(result);
});

serp::PPromise<T>

serp::PPromise<T> (persistent promise) behaves like a Promise but can be resolved multiple times. Each resolution delivers the value to the current set of subscribers. This is useful for streaming results or repeated async events.

serp::PPromise<SensorReading> sensorStream;

// Producer — called repeatedly:
sensorStream.resolve(currentReading);

// Consumer:
sensorStream.then([this](SensorReading r) {
    updateDisplay(r);
});

For most use cases, prefer serp::Property<T> or serp::Notification<Args...> over PPromise — they are integrated with the generated interface and transport layer. Use PPromise for internal async patterns that don't cross service boundaries.


Timeout Pattern

To reject a promise if a result does not arrive within a deadline, combine a serp::Timer (one-shot) with a shared serp::Promise:

auto promise = std::make_shared<serp::Promise<Result>>();

// Start the operation
beginOperation([promise](Result r) mutable {
    promise->resolve(r);
});

// Set a timeout
auto timeout = serp::Timer::create(eventLoop(), std::chrono::seconds(5), [promise]() mutable {
    promise->reject(serp::Error("operation timed out"));
});
timeout->setRepeating(false);
timeout->start();

// Consume
promise->then([this](Result r) {
    handleResult(r);
});

The first to fire (operation or timer) resolves/rejects the promise. The second call is silently ignored by the promise.


Pattern: Method Handler with Deferred Response

This is the canonical pattern for an async method implementation:

// Interface (generated)
serp::Call<MyService, serp::Response<std::string>, std::string> processItem;

// Implementation
processItem.setHandler([this](std::string input, serp::Response<std::string> response) {
    // Kick off async work without blocking the EventLoop
    m_worker.submit(input, [response](std::string result) mutable {
        // Called from worker thread; Response handles cross-thread delivery
        response.resolve(result);
    });
});

// Caller
myService.processItem("data", [](std::string result) {
    // Called on caller's EventLoop when the response resolves
    logInfo()->info("Result: " + result);
});

Promise Chaining for Sequential Steps

serp::Promise<Config> loadConfig()   { /* ... */ }
serp::Promise<bool>   applyConfig(Config) { /* ... */ }

loadConfig()
    .then([this](Config cfg) {
        return applyConfig(cfg);
    })
    .then([this](bool ok) {
        if (ok) {
            markReady();
        } else {
            logError()->error("Config apply failed");
        }
    });

Summary Table

Primitive Single-shot Multi-shot Cross-thread Integrates with serp::Call
Promise<T> Yes No Yes Via Response<T>
Response<T> Yes No Yes Yes — directly
Async<T> Yes No Yes Wraps Promise
PPromise<T> No Yes Yes Manual

See Also

Clone this wiki locally