docs/reference/detail/async-patterns.md
Promise lifetime management, background tasks, cancellation, and mutex usage in workerd.
.attach() for lifetime management — objects must outlive the promise that uses them:
// Correct: stream stays alive until readAllText() completes
return stream->readAllText().attach(kj::mv(stream));
// Wrong: stream destroyed immediately, promise has dangling reference
auto promise = stream->readAllText();
return promise; // stream is gone
.eagerlyEvaluate() for background tasks — without it, continuations are lazy
and may never run:
promise = doWork().then([]() {
KJ_LOG(INFO, "done"); // won't run unless someone .wait()s or .then()s
}).eagerlyEvaluate([](kj::Exception&& e) {
KJ_LOG(ERROR, e); // error handler is required
});
When using coroutines, eagerlyEvaluate() is implied and not needed to be called explicitly.
Use kj::TaskSet to manage many background tasks with a shared error handler.
Cancellation — destroying a kj::Promise cancels it immediately. No continuations
run, only destructors. Use .attach(kj::defer(...)) for cleanup that must happen
on both completion and cancellation.
kj::evalNow() — wraps synchronous code to catch exceptions as rejected promises:
return kj::evalNow([&]() {
// Any throw here becomes a rejected promise, not a synchronous exception
return doSomethingThatMightThrow();
});
kj::MutexGuarded<T> ties locking to access — you can't touch the data without a lock:
// Exclusive access for modification
{
auto lock = guarded.lockExclusive();
lock->modify();
// lock is released at end of scope
}
// Multiple readers ok
{
auto shared = guarded.lockShared();
shared->read();
// shared lock is released at end of scope
}
.wait(cond) on a lock replaces condition variables:
auto lock = guarded.lockExclusive();
lock.wait([](const T& val) { return val.ready; }); // releases/reacquires automatically
When using kj::MutexGuarded<T>, ensure the lock is correctly held for the duration of any
access to the guarded data.
Do this:
auto lock = mutexGuarded.lockExclusive();
KJ_IF_SOME(value, lock->maybeValue) {
// Access value safely while lock is held.
}
Not this:
KJ_IF_SOME(value, mutexGuarded.lockExclusive()->maybeValue) {
// Unsafe! The lock is released before we access maybeValue.
}