Keyboard Shortcuts

j / kScroll down / up
ggScroll to top
GScroll to bottom
K / /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

lib/config/network/app_error.dart
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

lib/config/network/error_interceptor.dart
class GlobalErrorCubit extends Cubit<AppError?> {
  final _seen = <String>{};

  GlobalErrorCubit() : super(null);

  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

lib/config/network/auth_interceptor.dart
class AuthInterceptor extends Interceptor {
  bool _isRefreshing = false;
  Completer<String?>? _refreshCompleter;

  @override
  Future<void> 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<String?>();

    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));
    }

    await sl<TokenManager>().clearTokens();
    handler.next(err);
  }
}

Global UI listener

lib/config/ui/global_error_listener.dart
// In app.dart — wraps entire app. One listener, zero duplication.
BlocListener<GlobalErrorCubit, AppError?>(
  listener: (context, error) {
    if (error != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(error.message)),
      );
      context.read<GlobalErrorCubit>().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