Skip to content

Async Operations

ferdi kurnaz edited this page Aug 27, 2026 · 2 revisions

Async Operations

Modern C# code often uses async/await, for example when calling a database or a web API. Resultron fully supports this: almost every method has an ...Async twin (MatchAsync, MapAsync, BindAsync), in addition to TryAsync.

Result.TryAsync (no return value)

Use this for an asynchronous action that does not return a value, like saving something:

var result = await Result.TryAsync(async () =>
{
    await _repository.SaveAsync(entity);
});

result.Match(
    onSuccess: () => Console.WriteLine("Saved."),
    onFailure: error => Console.WriteLine($"Error: {error.Description}")
);

If SaveAsync throws an exception, it is automatically caught and converted into a failed Result, exactly like the synchronous Try.

Result<T>.TryAsync (with a return value)

Use this when the asynchronous action returns a value:

Result<User> result = await Result<User>.TryAsync(async () =>
{
    return await _repository.GetByIdAsync(id);
});

result.Match(
    onSuccess: user => Console.WriteLine($"Found: {user.Name}"),
    onFailure: error => Console.WriteLine($"Error: {error.Description}")
);

Async chaining: MapAsync and BindAsync

Resultron now provides full async equivalents of Map and Bind, so you can build a chain where each step itself is asynchronous, without breaking out of the chain to await manually in between:

  • MapAsync — async version of Map. Use it for a step that transforms a value but does not return a Result by itself (it just happens to be async).
  • BindAsync — async version of Bind. Use it for a step that returns a Result/Result<T> wrapped in a Task (an async step that can itself succeed or fail).

Both exist on Result and on Result<T>, and both return a Task, so you await the whole chain, usually once at the end.

Example: a fully async pipeline

public async Task<Result<Order>> PlaceOrderAsync(Guid userId, Guid productId)
{
    var userResult = await Result<User>.TryAsync(() => _users.GetByIdAsync(userId));

    var validatedResult = await userResult.BindAsync(user => ValidateUserCanOrderAsync(user));

    var orderResult = await validatedResult.BindAsync(user => CreateOrderAsync(user, productId));

    return await orderResult.MapAsync(async order =>
    {
        await _auditLog.RecordAsync(order.Id);
        return order;
    });
}

Note: Each ...Async method returns a Task<Result<...>>, not a Result<...> directly. This is different from the synchronous Map/Bind, which can be chained one after another without any await in between. With MapAsync/BindAsync, you need to await each step before calling the next one on it, as shown above. This keeps the code simple and avoids nesting Tasks inside Results.

Mixing synchronous and asynchronous steps

You don't have to make every step async. Mix Map/Bind (sync) and MapAsync/BindAsync (async) freely, in whatever order your logic needs:

public async Task<Result<string>> GetWelcomeMessageAsync(Guid userId)
{
    var userResult = await Result<User>.TryAsync(() => _users.GetByIdAsync(userId));

    return userResult
        .Bind(user => ValidateUser(user))          // sync step
        .Map(user => user.Name)                    // sync step
        .Map(name => $"Welcome, {name}!");          // sync step
}
public async Task<Result> DeactivateUserAsync(Guid userId)
{
    var userResult = await Result<User>.TryAsync(() => _users.GetByIdAsync(userId));

    return await userResult
        .Bind(user => ValidateCanDeactivate(user))   // sync validation
        .BindAsync(user => _users.DeactivateAsync(user.Id)); // async save
}

Async Match

MatchAsync lets your success/failure branches themselves be async, for example to send a notification or write to a log asynchronously:

await result.MatchAsync(
    onSuccess: user => _notifier.SendWelcomeEmailAsync(user),
    onFailure: error => _logger.LogErrorAsync(error.Description)
);

Or, to compute a final value asynchronously:

string message = await result.MatchAsync(
    onSuccess: user => Task.FromResult($"Welcome, {user.Name}!"),
    onFailure: error => Task.FromResult($"Error: {error.Description}")
);

Full method list

Method Where it exists Purpose
Result.TryAsync(Func<Task>) Result Catch exceptions from an async action
Result<T>.TryAsync(Func<Task<T>>) Result<T> Catch exceptions from an async function
MatchAsync(...) Result, Result<T> React to success/failure asynchronously
MapAsync(...) Result, Result<T> Transform the value asynchronously
BindAsync(...) Result, Result<T> Chain to another async, Result-returning step

See the full method signatures on the Result API Reference and Result API Reference pages.

Next: see a full, realistic example in Full Example.

Clone this wiki locally