Claude-skill-registry asyncredux-user-exceptions

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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/asyncredux-user-exceptions" ~/.claude/skills/majiayu000-claude-skill-registry-asyncredux-user-exceptions && rm -rf "$T"
manifest: skills/data/asyncredux-user-exceptions/SKILL.md
source content

UserException in AsyncRedux

UserException
is a special error type for user-facing errors that should be displayed to the user rather than logged as bugs. These represent issues the user can address or should be informed about.

Throwing UserException from Actions

Throw

UserException
when an action encounters a user-facing error:

class TransferMoney extends AppAction {
  final double amount;
  TransferMoney(this.amount);

  AppState? reduce() {
    if (amount == 0) {
      throw UserException('You cannot transfer zero money.');
    }
    return state.copy(cash: state.cash - amount);
  }
}

For async actions with validation:

class SaveUser extends AppAction {
  final String name;
  SaveUser(this.name);

  Future<AppState?> reduce() async {
    if (name.length < 4)
      throw UserException('Name must have at least 4 letters.');

    await saveUser(name);
    return null;
  }
}

Converting Errors to UserException

Use

addCause()
to preserve the original error while showing a user-friendly message:

class ConvertAction extends AppAction {
  final String text;
  ConvertAction(this.text);

  Future<AppState?> reduce() async {
    try {
      var value = int.parse(text);
      return state.copy(counter: value);
    } catch (error) {
      throw UserException('Please enter a valid number')
        .addCause(error);
    }
  }
}

Setting Up UserExceptionDialog

Wrap your home page with

UserExceptionDialog
below both
StoreProvider
and
MaterialApp
:

Widget build(context) {
  return StoreProvider<AppState>(
    store: store,
    child: MaterialApp(
      home: UserExceptionDialog<AppState>(
        child: MyHomePage(),
      ),
    ),
  );
}

If you omit the

onShowUserExceptionDialog
parameter, a default dialog appears with the error message and an OK button.

Customizing Error Dialogs

Use

onShowUserExceptionDialog
to create custom error dialogs:

UserExceptionDialog<AppState>(
  onShowUserExceptionDialog: (BuildContext context, UserException exception) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Error'),
        content: Text(exception.message ?? 'An error occurred'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('OK'),
          ),
        ],
      ),
    );
  },
  child: MyHomePage(),
)

For non-standard error presentation (like snackbars or banners), you can modify the behavior by accessing the

didUpdateWidget
method in a custom implementation.

UserExceptionAction for Non-Interrupting Errors

Use

UserExceptionAction
to show an error dialog without throwing an exception or stopping action execution:

// Show error dialog without failing the action
dispatch(UserExceptionAction('Please enter a valid number'));

This is useful when you want to notify the user of an issue mid-action while continuing execution:

class ConvertAction extends AppAction {
  final String text;
  ConvertAction(this.text);

  Future<AppState?> reduce() async {
    var value = int.tryParse(text);
    if (value == null) {
      // Shows dialog but action continues
      dispatch(UserExceptionAction('Invalid number, using default'));
      value = 0;
    }
    return state.copy(counter: value);
  }
}

Reusable Error Handling with Mixins

Create mixins to standardize UserException conversion across actions:

mixin ShowUserException on AppAction {
  String getErrorMessage();

  Object? wrapError(Object error, StackTrace stackTrace) {
    return UserException(getErrorMessage()).addCause(error);
  }
}

class ConvertAction extends AppAction with ShowUserException {
  final String text;
  ConvertAction(this.text);

  @override
  String getErrorMessage() => 'Please enter a valid number.';

  Future<AppState?> reduce() async {
    var value = int.parse(text); // Any error becomes UserException
    return state.copy(counter: value);
  }
}

Global Error Handling with GlobalWrapError

Handle third-party or framework errors uniformly across all actions:

var store = Store<AppState>(
  initialState: AppState.initialState(),
  globalWrapError: MyGlobalWrapError(),
);

class MyGlobalWrapError extends GlobalWrapError {
  @override
  Object? wrap(Object error, StackTrace stackTrace, ReduxAction<dynamic> action) {
    if (error is PlatformException &&
        error.code == 'Error performing get') {
      return UserException('Check your internet connection')
        .addCause(error);
    }
    // Return the error unchanged for other cases
    return error;
  }
}

Processing order: Action's

wrapError()
->
GlobalWrapError
->
ErrorObserver

Error Queue

Thrown

UserException
instances are stored in a dedicated error queue within the store. The queue is consumed by
UserExceptionDialog
to display error messages. You can configure the maximum queue capacity in the Store constructor.

Checking Failed Actions in Widgets

Use these methods to check action failure status and display errors inline:

Widget build(BuildContext context) {
  if (context.isFailed(SaveUserAction)) {
    var exception = context.exceptionFor(SaveUserAction);
    return Column(
      children: [
        Text('Failed: ${exception?.message}'),
        ElevatedButton(
          onPressed: () {
            context.clearExceptionFor(SaveUserAction);
            context.dispatch(SaveUserAction(name));
          },
          child: Text('Retry'),
        ),
      ],
    );
  }
  return Text('User saved successfully');
}

Note: Error states automatically clear when an action is redispatched, so manual cleanup before retry is usually unnecessary.

Testing UserExceptions

Test that actions throw

UserException
correctly:

test('should throw UserException for invalid input', () async {
  var store = Store<AppState>(initialState: AppState.initialState());

  var status = await store.dispatchAndWait(TransferMoney(0));

  expect(status.isCompletedFailed, isTrue);
  var error = status.wrappedError;
  expect(error, isA<UserException>());
  expect((error as UserException).message, 'You cannot transfer zero money.');
});

Test multiple exceptions using the error queue:

test('should collect multiple UserExceptions', () async {
  var store = Store<AppState>(initialState: AppState.initialState());

  await store.dispatchAndWaitAll([
    InvalidAction1(),
    InvalidAction2(),
    InvalidAction3(),
  ]);

  var errors = store.errors;
  expect(errors.length, 3);
  expect(errors[0].message, 'First error message');
});

References

URLs from the documentation: