folly/coro/safe/docs/AsUnsafe.md
as_unsafe() and future->coro MigrationThe as_unsafe() method on now_task, now_task_with_executor, and safe_task
is an escape hatch for interoperating with older futures-based code or other
places not yet compatible with true structured concurrency patterns. as_unsafe()
should only be used temporarily during migrations, particularly when you need to
integrate futures-based automated testing with in-development coroutine implementations,
but should not be used in production code.
WARNING: The folly::coro::Task is not safe to pass references to.
now_task.as_unsafe() returns a folly::coro::Task. You must be careful with
lifetime management when passing references to as_unsafe().
now_task is safe by defaultWhen you use now_task normally (without as_unsafe()), the compiler forces
you to await it in the same full-expression that created it. This means C++
reference lifetime extension protects you from bugs:
// Safe: awaited immediately in the same expression
co_await someNowTask(localRef);
Once you call as_unsafe(), you exit this protected zone and must manually
reason through reference lifetimes.
This bug pattern often manifests as a heisenbug that only shows up in edge cases in production. It is very hard to track down if you don't run your service under ASAN.
Bug recipe:
.semi() or .start() (with now_task, these are gated behind
as_unsafe()). This often starts the task "in the background", on another
thread.Example of the bug:
now_task<int> processData(int& x);
folly::SemiFuture<int> BAD_processDataParent() {
// BUG: `x` is a reference that will go out of scope!
int x = 123;
return processData(x).as_unsafe().semi();
// The scope exits before the future completes,
// `processData` will be reading invalid memory.
}
Do not use as_unsafe() in production code unless you are in a very narrow
scenario where it is feasible to manually prove the resulting movable task is
used in a lifetime-safe way.
In order to safely pass a reference to an unsafe Task, this condition must hold:
The underlying memory is guaranteed to outlive the coroutine execution.
This condition can be achieved using co_invoke or deferValue. co_invoke
is strongly preferred — use deferValue only as an absolute last resort when you
cannot afford the extra memory cost of co_invoke.
The co_invoke coroutine frame is typically only a few dozen bytes larger. This difference is
unlikely to matter for the vast majority of use cases. Only consider deferValue in extremely
high-fanout scenarios where this memory overhead accumulates significantly.
WARNING: deferValue introduces additional safety risks. With deferValue, the task has
already started — the work runs in the background on some other executor. This makes it
very easy to fail to await completion, due to an unhandled exception or other subtle
control-flow bug. Failing to await completion is dangerous because:
You lose success-or-error information. Your background work might be failing 100% of the time, but there will be no recourse — errors are silently dropped.
You may violate implicit sequencing invariants. Something that expects to
run "after" the completion may run concurrently instead. While this might be
fine if you packaged up all task state in deferValue, in practice code
often relies on global state. Your background work might still be running
after the global state no longer expects it to (is destroyed, invalid, or
must not be mutated).
In the rare case where you must make a now_task take reference arguments
self-contained, use one of these patterns.
co_invoke (Strongly Preferred)Use co_invoke to make decay-copies of arguments into an additional coro frame (often on the heap),
guaranteeing their lifetime for the duration of the coroutine:
now_task<int> processData(int& x);
folly::SemiFuture<int> processDataSafe(int& x) {
// co_invoke makes decay-copies of arguments into a new coroutine frame
return folly::coro::co_invoke(
[](auto&&... args) {
return processData(std::forward<decltype(args)>(args)...)
.as_unsafe();
},
x)
.semi();
}
deferValue with Explicit Heap Allocation (Last Resort)If co_invoke is not suitable, you can use deferValue to extend the
lifetime of heap-allocated data:
now_task<int> processData(int& x);
folly::SemiFuture<int> processDataSafe(int& x) {
// Allocate on heap
auto dataPtr = std::make_unique<int>(x);
// Use deferValue to ensure dataPtr lives until the future completes
return processData(*dataPtr).as_unsafe().semi().deferValue(
[dataPtr = std::move(dataPtr)](int result) { return result; });
}
Note: The deferValue callback captures dataPtr, ensuring it stays alive
until the future is resolved.