Error
AsyncError<T> represents the failed state of an AsyncValue, containing the exception and stack trace produced by an unsuccessful asynchronous operation.
What is it?
AsyncError<T> is one of the three possible states of an AsyncValue.
It indicates that an asynchronous operation failed before producing a successful result.
Common reasons include:
- Network failures
- API errors
- Database exceptions
- File system errors
- Parsing failures
- Unexpected exceptions
Unlike throwing exceptions directly into the UI, Riverpod captures the failure inside an AsyncError, allowing your application to handle errors declaratively.
Why does it exist?
Without AsyncError, every asynchronous operation would require manual error handling using try-catch blocks.
For example:
try {
final user = await repository.fetchUser();
} catch (e) {
// Handle error
}
In a UI, this quickly becomes repetitive.
AsyncError centralizes error handling into the AsyncValue state, allowing the UI to react consistently to failures without surrounding every asynchronous operation with try-catch.
How Error Works
When an asynchronous operation throws an exception, Riverpod transitions from AsyncLoading to AsyncError.
Request Starts
│
▼
AsyncLoading
│
▼
Exception Thrown
│
▼
AsyncError
The error and its stack trace are preserved for debugging and reporting.
Syntax
FutureProvider
final userProvider = FutureProvider<User>((ref) async {
return repository.fetchUser();
});
Explanation:
- If
fetchUser()throws an exception, Riverpod automatically creates anAsyncError<User>.
Handling Errors with when()
final user = ref.watch(userProvider);
return user.when(
loading: () => const CircularProgressIndicator(),
data: (user) => Text(user.name),
error: (error, stackTrace) {
return Text(error.toString());
},
);
Explanation:
- The
errorcallback receives both the exception and its stack trace. - This ensures all failures are handled in one place.
Checking hasError
if (user.hasError) {
print(user.error);
}
Explanation:
hasErrorindicates whether the provider is currently in the error state.errorreturns the captured exception.
Error Lifecycle
AsyncLoading
│
▼
Exception
│
▼
AsyncError
The provider remains in the error state until it is refreshed, invalidated, or rebuilt.
Mental Model
Think of AsyncError as a delivery failure.
Order Placed
│
▼
Shipping
│
▼
Delivery Failed
The order didn't arrive successfully, but you receive information explaining what went wrong.
Similarly, AsyncError preserves the failure details instead of crashing the application.
Examples
Display an Error Message
user.when(
loading: () => const CircularProgressIndicator(),
data: (user) => Text(user.name),
error: (error, stackTrace) {
return Text('Failed to load user');
},
);
Explanation:
- Displays a user-friendly error message instead of crashing.
Log the Exception
user.when(
loading: () => const CircularProgressIndicator(),
data: (_) => const SizedBox(),
error: (error, stackTrace) {
debugPrint(error.toString());
return const ErrorScreen();
},
);
Explanation:
- Logs the error for debugging.
- Shows an error screen to the user.
Using hasError
if (user.hasError) {
return Text(user.error.toString());
}
Explanation:
- Useful for simple conditional rendering.
when()is usually preferred for exhaustive handling.
Real-World Example
final products = ref.watch(productsProvider);
return products.when(
loading: ProductGridSkeleton.new,
data: ProductGrid.new,
error: (error, stackTrace) {
return RetryView(
message: 'Unable to load products.',
);
},
);
Explanation:
- Displays a retry UI when the request fails.
- Improves the user experience by allowing recovery.
When to Use
Handle the error state when:
- Calling REST APIs.
- Reading databases.
- Working with streams.
- Loading local files.
- Performing any asynchronous operation that may fail.
When NOT to Use
Avoid using the error state for:
- Business validation (e.g., invalid form input).
- Simple synchronous conditions.
- Normal application flow.
Reserve AsyncError for genuine asynchronous failures.
Best Practices
- Always handle the error state.
- Display user-friendly error messages.
- Log detailed exceptions for debugging.
- Preserve the stack trace when reporting errors.
- Provide retry mechanisms where appropriate.
Common Mistakes
1. Ignoring Errors
❌ Wrong
user.when(
loading: LoadingView.new,
data: UserView.new,
error: (_, __) => const SizedBox(),
);
Why it's wrong:
- Users receive no indication that something went wrong.
✔ Correct
Display an informative error message or retry UI.
2. Showing Raw Exceptions to Users
❌ Wrong
Text(error.toString());
Why it's wrong:
- Exception messages are often technical.
- They may confuse users or expose implementation details.
✔ Correct
Text('Something went wrong. Please try again.');
Log the original exception separately for debugging.
3. Using Try-Catch in the UI
❌ Wrong
try {
final user = ref.watch(userProvider);
} catch (_) {}
Why it's wrong:
AsyncValuealready captures asynchronous exceptions.- UI should react to the
errorstate instead.
✔ Correct
Use the error callback in when().
4. Forgetting the Stack Trace
❌ Wrong
error: (error, _) {
report(error);
}
Why it's wrong:
- Stack traces help identify the source of failures.
✔ Correct
error: (error, stackTrace) {
report(error, stackTrace);
}
AsyncError vs AsyncLoading vs AsyncData
| State | Meaning | Contains Error |
|---|---|---|
AsyncLoading |
Operation in progress | ❌ |
AsyncData |
Operation succeeded | ❌ |
AsyncError |
Operation failed | ✅ |
Related APIs
AsyncValueAsyncLoadingAsyncDatawhen()maybeWhen()map()guard()FutureProviderStreamProviderAsyncNotifier
Summary
AsyncError<T> represents a failed asynchronous operation. Instead of throwing exceptions into the UI, Riverpod captures the error and its stack trace inside an AsyncValue, allowing applications to handle failures consistently. By responding to the error state with meaningful messages, logging, and retry options, you can build more resilient and user-friendly applications.