| j / k | Scroll down / up |
| gg | Scroll to top |
| G | Scroll to bottom |
| ⌘K / / | Open search |
| ? | Show this help |
| Esc | Close search / help |
| n / N | Next / previous section |
| h | Go to landing |
| d | Go to docs |
| : | Command mode — type section name |
Press ? or Esc to close
Two distinct catching layers — applies equally to Cubit and Bloc features. UI never sees a raw exception.
| Layer | Catches | How |
|---|---|---|
ErrorInterceptor | Network errors, HTTP status codes (401, 500…) | Dio interceptor, before Store is involved |
BaseStore | Parsing errors, DB failures, business logic, any unexpected exception | try/catch in execute() and executeWithCache() |
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;
} 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);
}
} 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);
}
} // 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,
); | Situation | Scope |
|---|---|
| Form validation failed | local — show inline under the field |
| API call failed | global — snackbar |
| Auth expired | global — redirect to login |
| Empty search results | Not an error — use empty state UI component |
ErrorScope.local errors are NOT sent to GlobalErrorCubit.