ExecuteAsync
Execute async actions with automatic error handling and lifecycle management
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).
- 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
Before & After Comparison
See how ExecuteAsync eliminates try-catch boilerplate:
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));
}
}
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:
Define State with AsyncData<T>
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;
}
Component with ExecuteAsync
@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
withexpressions 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:
// 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(), ...);
ExecuteAsync requires IAsyncActionExecutor<TState> to be registered.
Use AddStoreWithUtilities() for automatic registration, or manually call
AddAsyncActionExecutor<TState>() for each state type.