.claude/skills/asyncredux-wait-fail-succeed/SKILL.md
Show loading states and handle action failures in widgets. Covers `isWaiting(ActionType)` for spinners, `isFailed(ActionType)` for error states, `exceptionFor(ActionType)` for error messages, and `clearExceptionFor()` to reset failure states.
npx skillsauth add marcglasberg/async_redux asyncredux-wait-fail-succeedInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
AsyncRedux provides context extension methods to track async action states: waiting (in progress), failed (error), and succeeded (complete). These are essential for showing spinners, error messages, and success states in the UI.
| Method | Returns | Purpose |
|--------|---------|---------|
| isWaiting(ActionType) | bool | True if the action is currently running |
| isFailed(ActionType) | bool | True if the action recently failed |
| exceptionFor(ActionType) | UserException? | The exception from a failed action |
| clearExceptionFor(ActionType) | void | Manually clears stored exception |
Use isWaiting() to display a spinner while an action runs:
Widget build(BuildContext context) {
if (context.isWaiting(FetchDataAction)) {
return CircularProgressIndicator();
}
return Text('Data: ${context.state.data}');
}
The widget automatically rebuilds when the action starts and completes.
Use isFailed() and exceptionFor() to display error messages:
Widget build(BuildContext context) {
if (context.isFailed(FetchDataAction)) {
var exception = context.exceptionFor(FetchDataAction);
return Text('Error: ${exception?.message}');
}
return Text('Data: ${context.state.data}');
}
The typical pattern handles all three states:
Widget build(BuildContext context) {
// Loading state
if (context.isWaiting(GetItemsAction)) {
return Center(child: CircularProgressIndicator());
}
// Error state with retry
if (context.isFailed(GetItemsAction)) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Failed to load items'),
Text(context.exceptionFor(GetItemsAction)?.message ?? ''),
ElevatedButton(
onPressed: () => context.dispatch(GetItemsAction()),
child: Text('Retry'),
),
],
);
}
// Success state
return ListView.builder(
itemCount: context.state.items.length,
itemBuilder: (context, index) => ListTile(
title: Text(context.state.items[index].name),
),
);
}
When an action is dispatched again, any previous error for that action type is automatically cleared. This means:
isFailed() becomes false immediatelyisWaiting() becomes trueisFailed() becomes true with the new exceptionUse clearExceptionFor() when you need to dismiss an error without retrying:
Widget build(BuildContext context) {
if (context.isFailed(SubmitFormAction)) {
return AlertDialog(
title: Text('Error'),
content: Text(context.exceptionFor(SubmitFormAction)?.message ?? ''),
actions: [
TextButton(
onPressed: () {
context.clearExceptionFor(SubmitFormAction);
},
child: Text('Dismiss'),
),
TextButton(
onPressed: () => context.dispatch(SubmitFormAction()),
child: Text('Retry'),
),
],
);
}
// ...
}
Actions fail when they throw an error in before() or reduce(). Use UserException for user-facing errors:
class FetchDataAction extends ReduxAction<AppState> {
@override
Future<AppState?> reduce() async {
final response = await api.fetchData();
if (response.statusCode == 404) {
throw UserException('Data not found.');
}
if (response.statusCode != 200) {
throw UserException('Failed to load data. Please try again.');
}
return state.copy(data: response.data);
}
}
You can check multiple action types for waiting or failure:
Widget build(BuildContext context) {
// Check if any of several actions are running
bool isLoading = context.isWaiting(FetchUserAction) ||
context.isWaiting(FetchSettingsAction);
if (isLoading) {
return CircularProgressIndicator();
}
// Check for any failures
if (context.isFailed(FetchUserAction)) {
return Text('Failed to load user');
}
if (context.isFailed(FetchSettingsAction)) {
return Text('Failed to load settings');
}
return MyContent();
}
Combine with dispatchAndWait() for refresh indicators:
class MyListWidget extends StatelessWidget {
Future<void> _onRefresh(BuildContext context) {
return context.dispatchAndWait(RefreshItemsAction());
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () => _onRefresh(context),
child: ListView.builder(
itemCount: context.state.items.length,
itemBuilder: (context, index) => ListTile(
title: Text(context.state.items[index].name),
),
),
);
}
}
class LoadProductsAction extends ReduxAction<AppState> {
@override
Future<AppState?> reduce() async {
final products = await api.fetchProducts();
if (products.isEmpty) {
throw UserException('No products available.');
}
return state.copy(products: products);
}
}
class ProductsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: _buildBody(context),
floatingActionButton: FloatingActionButton(
onPressed: () => context.dispatch(LoadProductsAction()),
child: Icon(Icons.refresh),
),
);
}
Widget _buildBody(BuildContext context) {
if (context.isWaiting(LoadProductsAction)) {
return Center(child: CircularProgressIndicator());
}
if (context.isFailed(LoadProductsAction)) {
final error = context.exceptionFor(LoadProductsAction);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text(error?.message ?? 'An error occurred'),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.dispatch(LoadProductsAction()),
child: Text('Try Again'),
),
],
),
);
}
final products = context.state.products;
if (products.isEmpty) {
return Center(child: Text('No products yet. Tap refresh to load.'));
}
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) => ListTile(
title: Text(products[index].name),
subtitle: Text('\$${products[index].price}'),
),
);
}
}
URLs from the documentation:
data-ai
Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.
testing
Handle user-facing errors with UserException. Covers throwing UserException from actions, setting up UserExceptionDialog, customizing error dialogs with `onShowUserExceptionDialog`, and using UserExceptionAction for non-interrupting error display.
tools
Implement undo/redo functionality using state observers. Covers recording state history with stateObserver, creating a RecoverStateAction, implementing undo for the full state or partial state, and managing history limits.
tools
Add the Throttle mixin to prevent actions from running too frequently. Covers setting the throttle duration in milliseconds, use cases like price refresh, and how freshness/staleness works.