What is ExecuteAsync?

ExecuteAsync is a helper method that eliminates try-catch boilerplate when executing async operations. It automatically manages loading, success, and error states, reducing code by 58% (from 12 lines to 5 lines).

💡 Key Benefits
  • Automatic error handling with try-catch elimination
  • Built-in loading/success/error state transitions
  • Type-safe error callbacks
  • Composable with AsyncData<T> pattern
  • Reduces async boilerplate by 58%

Code Reduction Statistics

58%
Code Reduction
12 lines → 5 lines
100%
Try-Catch Elimination
Automatic error handling
3
State Transitions
Loading → Success → Error

Before & After Comparison

See how ExecuteAsync eliminates try-catch boilerplate:

❌ Before (12 lines)
async Task LoadData()
{
    await Update(s => s.StartLoading());
    try
    {
        var data = await Service.LoadAsync();
        await Update(s => s.Success(data));
    }
    catch (Exception ex)
    {
        await Update(s => s.Failure(ex.Message));
    }
}
✅ After (5 lines)
async Task LoadData() =>
    await ExecuteAsync(
        () => Service.LoadAsync(),
        loading: s => s with { Data = s.Data.ToLoading() },
        success: (s, data) => s with { Data = AsyncData.Success(data) },
        error: (s, ex) => s with { Data = AsyncData.Failure(ex.Message) }
    );

Complete Example

Real-world authentication example showing loading, success, and error states:

1

Define State with AsyncData<T>

AuthState.cs
public record User(string Id, string Name, string Email);

public record AuthState(AsyncData<User> CurrentUser)
{
    public static AuthState Initial => new(AsyncData<User>.NotAsked());
    public bool IsAuthenticated => CurrentUser.HasData;
}
2

Component with ExecuteAsync

Login.razor
@page "/login"
@inherits StoreComponent<AuthState>
@inject IAuthService AuthService

@if (State.CurrentUser.IsLoading)
{
    <p>Logging in...</p>
}
else if (State.IsAuthenticated)
{
    <p>Welcome, @State.CurrentUser.Data!.Name!</p>
    <button @onclick="Logout">Logout</button>
}
else
{
    <input @bind="email" placeholder="Email" />
    <input @bind="password" type="password" />
    <button @onclick="Login">Login</button>

    @if (State.CurrentUser.HasError)
    {
        <p class="error">@State.CurrentUser.Error</p>
    }
}

@code {
    string email = "", password = "";

    async Task Login() =>
        await ExecuteAsync(
            () => AuthService.LoginAsync(email, password),
            loading: s => s with { CurrentUser = s.CurrentUser.ToLoading() },
            success: (s, user) => s with { CurrentUser = AsyncData<User>.Success(user) },
            error: (s, ex) => s with { CurrentUser = AsyncData<User>.Failure(ex.Message) }
        );

    async Task Logout() => await Update(s => AuthState.Initial);
}

API Reference

ExecuteAsync<T>

protected Task ExecuteAsync<T>(
    Func<Task<T>> action,
    Func<TState, TState> loading,
    Func<TState, T, TState> success,
    Func<TState, Exception, TState>? error = null
)

Parameters

Parameter Type Description
action Func<Task<T>> The async operation to execute (e.g., API call)
loading Func<TState, TState> State transformation to apply when starting (loading state)
success Func<TState, T, TState> State transformation when operation succeeds (receives result)
error Func<TState, Exception, TState>? Optional error handler (if null, error is thrown)

Returns

Task - Completes when the operation and all state updates finish

Common Patterns

Pattern 1: With AsyncData<T> (Recommended)

public record ProductState(AsyncData<Product> Product);

async Task LoadProduct(int id) =>
    await ExecuteAsync(
        () => ProductService.GetAsync(id),
        loading: s => s with { Product = s.Product.ToLoading() },
        success: (s, data) => s with { Product = AsyncData.Success(data) },
        error: (s, ex) => s with { Product = AsyncData.Failure(ex.Message) }
    );

Pattern 2: Without Error Handling (Throws on Error)

async Task SaveData() =>
    await ExecuteAsync(
        () => Service.SaveAsync(State.Data),
        loading: s => s with { IsSaving = true },
        success: (s, result) => s with { IsSaving = false, LastSaved = DateTime.Now }
        // No error handler - exception will be thrown
    );

Pattern 3: Multiple Operations

async Task LoadAllData()
{
    await ExecuteAsync(
        () => UserService.GetCurrentUserAsync(),
        loading: s => s with { User = s.User.ToLoading() },
        success: (s, user) => s with { User = AsyncData.Success(user) }
    );

    await ExecuteAsync(
        () => ProductService.GetAllAsync(),
        loading: s => s with { Products = s.Products.ToLoading() },
        success: (s, products) => s with { Products = AsyncData.Success(products) }
    );
}

Pattern 4: Transforming Results

async Task SearchProducts(string query) =>
    await ExecuteAsync(
        () => ProductService.SearchAsync(query),
        loading: s => s with { Results = AsyncData<ImmutableList<Product>>.Loading() },
        success: (s, results) => s with {
            Results = AsyncData.Success(results.ToImmutableList()),
            SearchQuery = query,
            LastSearched = DateTime.Now
        },
        error: (s, ex) => s with {
            Results = AsyncData<ImmutableList<Product>>.Failure(ex.Message),
            LastError = ex.Message
        }
    );

Best Practices

✅ Do

  • Combine with AsyncData<T> for complete async state management
  • Use named parameters for clarity (loading:, success:, error:)
  • Provide error handlers for user-facing operations
  • Keep state transformations pure (no side effects)
  • Use with expressions for immutable updates
  • Chain multiple ExecuteAsync calls for sequential operations

❌ Don't

  • Don't nest try-catch inside ExecuteAsync (redundant)
  • Don't mutate state in callbacks - use with
  • Don't forget to register utilities: AddStoreWithUtilities()
  • Don't perform side effects in state transformations
  • Don't omit error handlers for critical operations
  • Don't mix ExecuteAsync with manual try-catch

Advanced Usage

Combining with Other Async Helpers

@page "/products"
@inherits StoreComponent<ProductState>

<!-- 1. Debounced search input -->
<input @oninput="@(e => UpdateDebounced(s => s.SetQuery(e.Value?.ToString() ?? ""), 300))"
       placeholder="Search products..." />

<!-- 2. Search button with ExecuteAsync -->
<button @onclick="Search">Search</button>

<!-- 3. Display results with AsyncData -->
@if (State.Products.IsLoading) { <p>Loading...</p> }
@if (State.Products.HasData)
{
    @foreach (var product in State.Products.Data)
    {
        <div @onclick="@(() => LoadDetails(product.Id))">@product.Name</div>
    }
}

@code {
    async Task Search() =>
        await ExecuteAsync(
            () => ProductService.SearchAsync(State.Query),
            loading: s => s with { Products = s.Products.ToLoading() },
            success: (s, data) => s with { Products = AsyncData.Success(data) }
        );

    async Task LoadDetails(int id)
    {
        // 4. LazyLoad with caching
        var details = await LazyLoad(
            $"product-{id}",
            () => ProductService.GetDetailsAsync(id),
            TimeSpan.FromMinutes(5));

        await Update(s => s.AddDetails(id, details));
    }
}

Custom Error Handling Logic

async Task SaveWithRetry() =>
    await ExecuteAsync(
        () => Service.SaveAsync(State.Data),
        loading: s => s with { IsSaving = true },
        success: (s, result) => s with { IsSaving = false, LastSaved = DateTime.Now },
        error: (s, ex) =>
        {
            if (ex is UnauthorizedException)
                return s with { Error = "Please login again", NeedsReauth = true };
            else if (ex is ValidationException valEx)
                return s with { ValidationErrors = valEx.Errors.ToImmutableList() };
            else
                return s with { Error = "An unexpected error occurred" };
        }
    );

Registration

ExecuteAsync requires async utilities to be registered:

Program.cs
// Recommended: All-in-one registration
builder.Services.AddStoreWithUtilities(
    new MyState(),
    (store, sp) => store.WithDefaults(sp, "MyStore"));

// Manual registration (if needed)
builder.Services.AddStoreUtilities();
builder.Services.AddAsyncActionExecutor<MyState>();
builder.Services.AddStore(new MyState(), ...);
⚠️ Required Services

ExecuteAsync requires IAsyncActionExecutor<TState> to be registered. Use AddStoreWithUtilities() for automatic registration, or manually call AddAsyncActionExecutor<TState>() for each state type.

Next Steps