Async
Chain asynchronous work through Option and Result without awaiting every step.
Most operations have an async counterpart with an Async suffix. Use one when your transform, predicate, or side effect returns a Task. The full list is at the bottom of this page.
The receiver is the task, not the monad
This is the part worth understanding, and it is not obvious.
Most async methods are extension methods on the task that wraps the monad, not on the monad itself. They extend Task<Option<T>>, ValueTask<Option<T>>, Task<Result<T, E>> and ValueTask<Result<T, E>>.
That is what lets you chain without awaiting each step:
// no intermediate awaits, one await at the end
Character character = await SummonCharacterAsync(id)
.MapAsync(c => EnrichAsync(c))
.UnwrapOrAsync(Commoner);Without them, you would await into a local at every step:
Option<Character> fetched = await SummonCharacterAsync(id);
Option<Character> enriched = await fetched.MapAsync(c => EnrichAsync(c));
Character character = enriched.UnwrapOr(Commoner);Two things about that first sample, because both catch people out:
EnrichAsyncmust returnTask<Character>.MapAsynctakesFunc<T, Task<TOut>>or a plainFunc<T, TOut>. It does not take aValueTaskfactory.Commoneris a value, not a function.UnwrapOrAsynctakesT, the same asUnwrapOr. Pass a method group and you getCS0411.
MatchAsync is the exception — it also extends the monad directly, so you can pass an async branch to an Option<T> or Result<T, E> you already hold. See Matching with an async branch.
Every async member returns ValueTask
From 7.0.0 the rule has no exceptions: every async member on Option and Result returns ValueTask or ValueTask<T>. That includes the static factories. Option.TryAsync, Result.TryAsync and CollectAsync returned Task up to 6.7.0 — see Loud change: TryAsync and CollectAsync return ValueTask.
You await a ValueTask the same way you await a Task, so this rarely changes your code. Two things to know:
Await it once, and only once. This matters if you store it before awaiting.
Call
.AsTask()when you need aTask— most often forTask.WhenAll.
Both Task and ValueTask work as receivers, so a chain that mixes them still composes.
Create a monad from async work
TryAsync captures a factory that returns a Task and may throw.
If the factory throws, the exception is caught and sent to your configured exception logger, and you get back a None<Character>.
You also get a None<Character> if the task completes with null, because a Some cannot hold one. Nothing is logged in that case, because nothing threw.
The single type parameter overload converts the exception with Error.FromException, so you do not pass an onError delegate.
TryAsync also calls onError when the task completes with null, passing you an ArgumentNullException that names the asyncFactory argument.
Never pass an async factory to Try. The overloads that accepted one were removed in 6.0.0, and the call still compiles — it binds to the synchronous overload, gives you an Option<Task<T>>, and catches nothing. WM1011 reports every occurrence. See Silent change 1.
TryAsync lets an OperationCanceledException through rather than turning it into a None or an Err. See Configuration.
Matching with an async branch
MatchAsync works on a monad you already hold, not only on a task that wraps one. Use it when one or both branches do async work.
Pick the overload that matches your branches. A branch you write as a plain value stays a plain value instead of being wrapped in a completed task, and the branch that does not match never runs.
The same three shapes work on Task<Option<T>> and ValueTask<Option<T>> receivers, so a chain reaches them too.
Option and Result cover different combinations
This catches people out, so check the table before you write the call:
Branches
Option<T>
Result<T, E>
Both async, returning a value
Yes
Yes
One async, returning a value
Yes
No
Async, returning nothing
No
Yes
The middle row is the one that bites. This does not compile:
There is no Result overload taking one async branch and one plain branch that returns a value, so the call binds to the overload returning nothing. You get CS0029 on the assignment, which points at the line but not at the cause. Make both branches async, or match on an Option<T>.
End a chain
The consuming operations have async counterparts too, so you can finish a chain without awaiting the monad first.
UnwrapOrDefaultAsync returns T?, not T. Assign it to a nullable local. With nullable reference types on, Character orDefault = … is CS8600.
The full surface
Every method below behaves exactly like the synchronous version documented in Option<T> and Result<T, E>. The only difference is that it accepts an async delegate, a task receiver, or both.
Option<T>
Transform
MapAsync, MapOrAsync, MapOrDefaultAsync, MapOrElseAsync, AndThenAsync
State checks
IsSomeAndAsync, IsNoneOrAsync
Consume
MatchAsync, UnwrapAsync, UnwrapOrAsync, UnwrapOrElseAsync, UnwrapOrDefaultAsync, ExpectAsync
Side effect
InspectAsync
Filter and combine
FilterAsync, ZipWithAsync, OrElseAsync
Nesting
FlattenAsync
Conversion
OkOrAsync, OkOrElseAsync
Result<T, E>
Transform
MapAsync, MapErrAsync, MapOrAsync, MapOrDefaultAsync, MapOrElseAsync
State checks
IsOkAndAsync, IsErrAndAsync
Consume
MatchAsync, UnwrapAsync, UnwrapErrAsync, UnwrapOrAsync, UnwrapOrElseAsync, UnwrapOrDefaultAsync, ExpectAsync, ExpectErrAsync
Side effect
InspectAsync, InspectErrAsync
Logical operators
AndThenAsync, OrElseAsync
Nesting
FlattenAsync
Last updated
Was this helpful?