| j / k | Scroll down / up |
| gg | Scroll to top |
| G | Scroll to bottom |
| / | 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{ 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); } }
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);
}
} // 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, );
| 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.