Keyboard Shortcuts

j / kScroll down / up
ggScroll to top
GScroll to bottom
/Open search
?Show this help
EscClose search / help
n / NNext / previous section
hGo to landing
dGo to docs
:Command mode — type section name

Press ? or Esc to close

Deep Dives

Error Handling

Two distinct catching layers — applies equally to Cubit and Bloc features. UI never sees a raw exception.

LayerCatchesHow
ErrorInterceptorNetwork errors, HTTP status codes (401, 500…)Dio interceptor, before Store is involved
BaseStoreParsing errors, DB failures, business logic, any unexpected exceptiontry/catch in execute() and executeWithCache()

AppError model

enum ErrorScope { local, global }
enum ErrorType  { network, auth, business, unknown }

class AppError {
  final String code;      // unique ID for deduplication
  final String message;   // user-facing text
  final ErrorScope scope; // local = inline UI, global = snackbar
  final ErrorType type;
}

GlobalErrorCubit — with deduplication

class GlobalErrorCubit extends Cubit {
  final _seen = {};

  void emitError(AppError error) {
    if (_seen.contains(error.code)) return; // deduplicate
    _seen.add(error.code);
    emit(error);
  }

  void clear() { _seen.clear(); emit(null); }
}

401 Refresh lock — Completer pattern

When multiple requests fire simultaneously and all get 401, without protection you get multiple parallel refresh calls. The solution is refresh locking via a Completer.

class AuthInterceptor extends Interceptor {
  bool _isRefreshing = false;
  Completer? _refreshCompleter;

  @override
  Future onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode != 401) return handler.next(err);

    if (_isRefreshing) {
      // already refreshing — wait, then retry
      final newToken = await _refreshCompleter!.future;
      if (newToken != null) {
        err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
        return handler.resolve(await _retry(err.requestOptions));
      }
      return handler.next(err);
    }

    _isRefreshing = true;
    _refreshCompleter = Completer();

    final newToken = await _doRefresh();
    _refreshCompleter!.complete(newToken);  // all waiters get the token
    _isRefreshing = false;
    _refreshCompleter = null;

    if (newToken != null) {
      err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
      return handler.resolve(await _retry(err.requestOptions));
    }

    _redirectToLogin(); // refresh failed → logout
    handler.next(err);
  }
}

Global UI listener

// In app.dart — wraps entire app. One listener, zero duplication.
BlocListener(
  listener: (context, error) {
    if (error != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(error.message)),
      );
      context.read().clear();
    }
  },
  child: child,
);

Error scope — when to use local vs global

SituationScope
Form validation failedlocal — show inline under the field
API call failedglobal — snackbar
Auth expiredglobal — redirect to login
Empty search resultsNot an error — use empty state UI component
Constraints:
Never throw raw exceptions to UI.
Never show error dialogs from business logic.
Cubit never catches errors silently without emitting state.
Retry is opt-in per call — not default everywhere.
ErrorScope.local errors are NOT sent to GlobalErrorCubit.
sam's arch — A Pragmatic Flutter Architecture