Error Handling
Stop worrying about uncaught exceptions crashing your app. command_it provides automatic exception handling with powerful routing capabilities - no more messy try-catch blocks or Result<T, Error> types everywhere.
Key Features:
- 🛡️ Never worry about exceptions - Commands catch all errors automatically
- 🎯 Powerful error routing - Route errors locally, globally, or let them throw
- 🎁 Stop returning Result types - Functions return clean
T, notResult<T, Error> - 📡 Reactive error handling - Observable
Streams andValueListenablefor errors - 🔧 Flexible filters - Configure per-command or global error handling strategies
From basic error listening to advanced routing patterns, command_it gives you complete control over how your app handles failures.
Don't Be Intimidated!
This documentation is comprehensive, but error handling in command_it is actually simple once you understand the core principle: errors are just data that flows through your app. Start with Basic Error Handling below - you can listen to .errors just like any other property.
Basic Error Handling
If the wrapped function inside a Command throws an exception, the command catches it so your app won't crash. Instead, it wraps the caught error together with the parameter value in a CommandError object and assigns it to the command's .errors property.
The .errors Property
Commands expose a .errors property of type ValueListenable<CommandError?>:
Behavior:
.errorsis reset tonullat the start of execution (does not notify listeners).errorsis set toCommandError<TParam>on failure (notifies listeners)CommandErrorcontains:error,paramData,stackTrace
Pattern 1: Display Error State with watchValue
Watch the error value to display it in your UI:
class DataWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final error = watchValue((DataManager m) => m.loadData.errors);
if (error != null) {
return Text(
'Error: ${error.error}',
style: TextStyle(color: Colors.red),
);
}
return Text('No errors');
}
}Pattern 2: Handle Errors with registerHandler
Use registerHandler for side effects like showing toasts or snackbars:
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Show snackbar with retry button when error occurs
registerHandler(
select: (TodoManager m) => m.loadTodos.errors,
handler: (context, error, cancel) {
if (error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${error.error}'),
action: SnackBarAction(
label: 'Retry',
onPressed: () => di<TodoManager>().loadTodos(error.paramData),
),
),
);
}
},
);
return TodoList();
}
}Pattern 3: Listen Directly on Command Definition
Chain .listen() when defining commands for logging or analytics:
class DataManager {
late final loadData = Command.createAsyncNoParam<List<Item>>(
() => api.fetchData(),
[],
)..errors.listen((error, _) {
if (error != null) {
debugPrint('Load failed: ${error.error}');
analytics.logError(error.error, error.stackTrace);
}
});
}These patterns are referred to as local error handling because they handle errors for one specific command. This gives you fine-grained control over how each command's errors are handled. For handling errors from multiple commands in one place, see Global Error Handler below.
Error Clearing Behavior
The .errors property normally never notifies with a null value unless you explicitly call clearErrors(). You normally never need to call clearErrors() - and if you don't, you don't need to add if (error != null) checks in your error handlers. See clearErrors for details.
Without watch_it
For StatefulWidget patterns using .listen() in initState, see Without watch_it for patterns.
Using CommandResult
You can also access errors through .results which combines all command state:
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final result = watchValue((TodoManager m) => m.loadTodos.results);
if (result.hasError) {
return ErrorWidget(
error: result.error!,
query: result.paramData,
onRetry: () => di<TodoManager>().loadTodos(result.paramData),
);
}
// ... handle other states
}
}See Command Results for details.
Global Error Handler
Set a global error handler to catch all command errors routed by ErrorFilter:
static void Function(CommandError<dynamic> error, StackTrace stackTrace)?
globalExceptionHandler;Access to Error Context
CommandError<TParam> provides rich context:
.error- The actual exception thrown.commandName- Command name/identifier (fromdebugName).paramData- Parameter passed to command.stackTrace- Full stack trace.errorReaction- How the error was handled
Handling Specific Error Types
You can handle different error types centrally in your global handler. A common pattern is handling authentication errors by logging out and cleaning up scopes:
void setupGlobalExceptionHandler() {
Command.globalExceptionHandler = (commandError, stackTrace) {
final error = commandError.error;
// Handle auth errors: logout and clean up
if (error is AuthException) {
// Logout (clears tokens, user state, etc.)
getIt<UserManager>().logout();
// Pop the 'loggedIn' scope to dispose logged-in services
// See: https://flutter-it.dev/documentation/get_it/scopes
getIt.popScope();
// Navigate to login screen would happen via app-level observer
// watching userManager.isLoggedIn
return;
}
// Handle other errors: log to crash reporter
crashReporter.logError(error, stackTrace);
};
}This centralizes authentication cleanup - any command that throws AuthException will automatically trigger logout, regardless of where it's called.
Usage with Crash Reporting
void setupGlobalExceptionHandler() {
Command.globalExceptionHandler = (commandError, stackTrace) {
// Access all error context from CommandError
final error = commandError.error; // The actual exception thrown
final command = commandError.command; // Command name/identifier
final param = commandError.paramData; // Parameter passed to command
final reaction = commandError.errorReaction; // How error was handled
// Send to Sentry with rich context
Sentry.captureException(
error,
stackTrace: stackTrace,
withScope: (scope) {
// Add tags for filtering in Sentry UI
scope.setTag('command', command?.toString() ?? 'unknown');
scope.setTag('error_type', error.runtimeType.toString());
// Add context for debugging
scope.setContexts('command_context', {
'command_name': command,
'command_parameter': param?.toString(),
'error_reaction': reaction.toString(),
});
},
);
// Log for debugging
if (kDebugMode) {
debugPrint('Command "$command" failed');
debugPrint('Parameter: $param');
debugPrint('Error: $error');
debugPrint('Reaction: $reaction');
}
};
}When the global handler is called depends on your ErrorFilter configuration. See Built-in Filters for details.
Error Filters
Error filters decide how each error should be handled: by a local handler, the global handler, both, or not at all. Instead of treating all errors the same, you can route them declaratively based on type or conditions.
Why Use Error Filters?
Different error types need different handling:
- Validation errors → Show to user in UI
- Network errors → Retry logic or offline mode
- Authentication errors → Redirect to login
- Critical errors → Log to monitoring service
- All without scattered try/catch blocks
Two Approaches to Error Filtering
Commands support two mutually exclusive ways to specify error filtering logic:
Function-based approach (errorFilterFn) - Direct function with compile-time type safety:
typedef ErrorFilterFn = ErrorReaction? Function(
Object error,
StackTrace stackTrace,
);
Command.createAsync(
fetchData,
[],
errorFilterFn: (e, s) => e is NetworkException
? ErrorReaction.globalHandler
: null,
// Compile-time checked signature! ✅
);Class-based approach (errorFilter) - ErrorFilter objects for complex logic:
Command.createAsync(
fetchData,
[],
errorFilter: PredicatesErrorFilter([
(e, s) => e is NetworkException ? ErrorReaction.globalHandler : null,
(e, s) => e is ValidationException ? ErrorReaction.localHandler : null,
]),
);command_it provides built-in filter classes (PredicatesErrorFilter, TableErrorFilter, etc.), but you can also define your own by implementing the ErrorFilter interface.
Key differences:
| Feature | errorFilterFn (Function) | errorFilter (Class) |
|---|---|---|
| Simplicity | ✅ Direct inline function | Requires object creation |
| With parameters | ❌️ Needs lambda wrapper | ✅ Can be const objects |
| Reusability | ❌️ Creates new closure each time | ✅ Reuse same const instance |
| Best for | Simple, one-off filters | Parameterized, reusable filters |
Mutually Exclusive
You cannot use both errorFilter and errorFilterFn on the same command - an assertion enforces this. Choose one approach based on your needs.
ErrorReaction Enum
An ErrorFilter returns an ErrorReaction to specify handling:
| Reaction | Behavior |
|---|---|
| localHandler | Call listeners on .errors/.results |
| globalHandler | Call Command.globalExceptionHandler |
| localAndGlobalHandler | Call both handlers |
| firstLocalThenGlobalHandler | Try local, fallback to global (default) |
| throwException | Rethrow immediately (debugging only) |
| throwIfNoLocalHandler | Throw if no listeners |
| noHandlersThrowException | Throw if no handlers present |
| none | Swallow silently |
Simple Error Filters
Built-in const filters for common routing patterns:
| Filter | Behavior | Usage |
|---|---|---|
| ErrorFilterConstant | Always returns same ErrorReaction | const ErrorFilterConstant(ErrorReaction.none) |
| LocalErrorFilter | Route to local handler only | const LocalErrorFilter() |
| GlobalIfNoLocalErrorFilter | Try local, fallback to global (default) | const GlobalIfNoLocalErrorFilter() |
| LocalAndGlobalErrorFilter | Route to both local and global handlers | const LocalAndGlobalErrorFilter() |
Example:
// Silent failure for background sync
late final backgroundSync = Command.createAsyncNoParam<void>(
() => api.syncInBackground(),
errorFilter: const ErrorFilterConstant(ErrorReaction.none),
);
// Debug: throw on error to catch in debugger
late final debugCommand = Command.createAsync<Data, void>(
(data) => api.saveCritical(data),
errorFilter: const ErrorFilterConstant(ErrorReaction.throwException),
);Understanding GlobalIfNoLocalErrorFilter (The Default)
Why this is the default: The GlobalIfNoLocalErrorFilter provides smart routing that adapts to your code. It returns firstLocalThenGlobalHandler, which works like this:
How it works:
- Checks if local listeners exist - Are you handling
.errorsor.resultsfor this command (listen, watchValue, registerHandler)? - If YES → Routes to local handler only (assumes you're handling it)
- If NO → Falls back to global handler (prevents silent failures)
Why this matters:
// Example 1: Has local error handler
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Local listener exists
final error = watchValue((TodoManager m) => m.loadTodos.errors);
// ✅ Errors route to LOCAL handler only
if (error != null) return ErrorWidget(error);
return TodoList();
}
}
// Example 2: No local error handler
class DataManager {
late final loadData = Command.createAsyncNoParam<List<Item>>(
() => api.fetchData(),
[],
);
// ❌ No .errors/.results listeners
// ✅ Errors route to GLOBAL handler automatically
}This prevents the common mistake of forgetting to handle errors - they'll at least reach your global crash reporter. If you add a local handler later, the global handler automatically stops being called for that command.
See the exception handling flowchart for the complete decision flow.
Custom ErrorFilter
Build your own ErrorFilters for advanced routing:
// Handle 4xx client errors locally, let 5xx go to global handler
late final fetchUserCommand = Command.createAsync<String, User>(
(userId) => api.fetchUser(userId),
initialValue: User.empty(),
errorFilter: _ApiErrorFilter([400, 401, 403, 404, 422]),
);
class _ApiErrorFilter implements ErrorFilter {
final List<int> statusCodes;
const _ApiErrorFilter(this.statusCodes);
@override
ErrorReaction filter(Object error, StackTrace stackTrace) {
if (error is ApiException && statusCodes.contains(error.statusCode)) {
return ErrorReaction.localHandler;
}
return ErrorReaction.defaulErrorFilter;
}
}More Error Filters
PredicatesErrorFilter (Recommended)
Chain predicates to match errors by type hierarchy:
class DataService {
final api = ApiClient();
int requestCount = 0;
// Command with ErrorFilter for different error types
late final loadDataCommand = Command.createAsyncNoParam<List<Todo>>(
() async {
await simulateDelay();
requestCount++;
// Simulate different error scenarios
if (requestCount == 1) {
throw ValidationException('Invalid request');
} else if (requestCount == 2) {
throw ApiException('Network timeout', 408);
} else if (requestCount == 3) {
throw Exception('Unknown error');
}
return fakeTodos;
},
initialValue: [],
errorFilter: PredicatesErrorFilter([
// Validation errors: handled locally (show to user)
(error, stackTrace) {
if (error is ValidationException) {
return ErrorReaction.localHandler;
}
return null;
},
// API errors with retry-able status: local handler
(error, stackTrace) {
if (error is ApiException && error.statusCode == 408) {
return ErrorReaction.localHandler;
}
return null;
},
// Other API errors: send to global handler (logging)
(error, stackTrace) {
if (error is ApiException) {
return ErrorReaction.globalHandler;
}
return null;
},
// Unknown errors: both local and global
(error, stackTrace) => ErrorReaction.localAndGlobalHandler,
]),
);
}
class ErrorFilterWidget extends StatefulWidget {
const ErrorFilterWidget({super.key});
@override
State<ErrorFilterWidget> createState() => _ErrorFilterWidgetState();
}
class _ErrorFilterWidgetState extends State<ErrorFilterWidget> {
final service = DataService();
String? lastError;
@override
void initState() {
super.initState();
// Set up global error handler
Command.globalExceptionHandler = (error, stackTrace) {
debugPrint('Global handler caught: $error');
// In real app: send to logging service
};
// Listen to local errors
service.loadDataCommand.errors.listen((error, _) {
setState(() {
lastError = error?.error.toString();
});
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Error Filter Example',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
if (lastError != null)
Card(
color: Colors.red.shade50,
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(width: 8),
Expanded(child: Text(lastError!)),
],
),
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: service.loadDataCommand.run,
child: Text('Load Data (attempt ${service.requestCount + 1})'),
),
SizedBox(height: 8),
Text(
'Try loading multiple times to see different error types:\n'
'1st: ValidationException (local)\n'
'2nd: ApiException 408 (local)\n'
'3rd: Exception (both handlers)\n'
'4th: Success',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
);
}
}How it works:
- Predicates are functions:
(error, stackTrace) => ErrorReaction? - Returns first non-null reaction
- Falls back to default if none match
- Order matters - check specific types first
Pattern:
PredicatesErrorFilter([
(error, stackTrace) => errorFilter<ApiException>(
error,
ErrorReaction.localHandler,
),
(error, stackTrace) => errorFilter<ValidationException>(
error,
ErrorReaction.localHandler,
),
(error, stackTrace) => ErrorReaction.globalHandler, // Default
])Prefer this for most cases - it's more flexible than TableErrorFilter.
TableErrorFilter
Map error types to reactions using exact type equality:
errorFilter: TableErrorFilter({
ApiException: ErrorReaction.localHandler,
ValidationException: ErrorReaction.localHandler,
NetworkException: ErrorReaction.globalHandler,
Exception: ErrorReaction.globalHandler,
})Limitations:
- Only matches exact runtime type (not type hierarchy)
- Can't distinguish subclasses
- Special workaround for
Exceptiontype
When to use:
- Simple error routing by type
- Known set of error types
- No inheritance hierarchies
Error Behavior with runAsync()
When using runAsync() and the command throws an exception, both things happen:
- Error handlers are called -
.errorslisteners andglobalExceptionHandlerreceive the error (based on ErrorFilter) - The Future completes with error - The exception is rethrown to the caller
Important: You MUST wrap runAsync() in try/catch to prevent app crashes:
// ✅ GOOD: Catch the rethrown exception
try {
final result = await loadCommand.runAsync();
// Use result...
} catch (e) {
// Handle the error - show UI feedback, log, etc.
showErrorToast(e.toString());
}
// ❌ BAD: Unhandled exception will crash the app
await loadCommand.runAsync(); // If this throws, app crashes!Using both try/catch and .errors listener:
If you have an .errors listener for reactive UI updates, you still need try/catch but the catch block can be empty:
// Set up error listener for reactive UI
loadCommand.errors.listen((error, _) {
if (error != null) showErrorToast(error.error);
});
// Still need try/catch to prevent crash
try {
final result = await loadCommand.runAsync();
// Use result...
} catch (e) {
// Error already handled by .errors listener above
// Empty catch just prevents the crash
}ErrorReaction.none Not Allowed
Using ErrorReaction.none with runAsync() will trigger an assertion error. Since the error would be swallowed, there's no value to complete the Future with.
When Error Handlers Throw Exceptions
Error handlers are regular Dart code - they can fail too. When your error handler makes async API calls or processes data, those operations can throw exceptions.
The Problem
Error handlers that perform side effects can fail in many scenarios:
- ⚠️ Async operations - Logging to remote services that might timeout
- ⚠️ Data processing - Parsing or formatting errors
Without proper handling, these secondary exceptions could crash your app or go unnoticed.
reportErrorHandlerExceptionsToGlobalHandler
Control whether exceptions thrown inside error handlers are reported to the global handler:
class DataManager {
final api = ApiClient();
final errorLoggingApi = ErrorLoggingApi();
// Error handler that makes async API call - can throw!
late final loadDataCommand = Command.createAsyncNoParam<List<Todo>>(
() async {
await simulateDelay();
throw ApiException('Failed to load data', 500);
},
initialValue: [],
)..errors.listen((error, _) async {
if (error != null) {
try {
// This async operation can fail with NetworkException
await errorLoggingApi.logError(error);
} catch (e) {
// If reportErrorHandlerExceptionsToGlobalHandler is true (default),
// this exception will be caught and sent to globalExceptionHandler
// with originalError set to the command's error
rethrow;
}
}
});
}
void setupGlobalHandler() {
Command.globalExceptionHandler = (error, stackTrace) {
if (error.originalError != null) {
// This is an exception from an error handler, not the command itself
debugPrint('''
Error Handler Failed:
- Handler exception: ${error.error}
- Original command error: ${error.originalError}
- Command: ${error.commandName}
''');
// Log to monitoring service, show alert, etc.
} else {
// Normal command error
debugPrint('Command failed: ${error.error}');
}
};
// Enable reporting (this is the default, shown for clarity)
Command.reportErrorHandlerExceptionsToGlobalHandler = true;
}Configuration:
// In main() or app initialization (this is the default)
Command.reportErrorHandlerExceptionsToGlobalHandler = true;See Global Configuration - reportErrorHandlerExceptionsToGlobalHandler for details.
How It Works
With true (default, recommended):
- ✅ Exceptions in error handlers are automatically caught
- ✅ Sent to
Command.globalExceptionHandler - ✅ Original command error preserved in
CommandError.originalError - ✅ Your app doesn't crash from buggy error handling code
With false:
- ❌️ Only logged by Flutter error logger
- ❌️ Won't reach your global exception handler
- ❌️ Less visibility into error handler bugs
How This Works
The .errors property is a CustomValueNotifier from listen_it, which provides the built-in ability to catch exceptions thrown by listeners. You can use this same feature in your own code with CustomValueNotifier - see listen_it CustomValueNotifier for details.
Production Recommendation
Always keep reportErrorHandlerExceptionsToGlobalHandler: true in production. Error handler failures indicate bugs in your error handling code that need immediate attention.
Global Errors Stream
Static Stream on the Command class for all command errors routed to the global handler:
static Stream<CommandError<dynamic>> get globalErrorsOverview
A broadcast stream that emits CommandError<dynamic> for every error that would trigger globalExceptionHandler. Perfect for centralized error monitoring, analytics, crash reporting, and global UI notifications.
Stream Behavior
Emits when:
- ✅️
ErrorFilterroutes error to global handler (based on filter configuration) - ✅️ Error handler itself throws an exception (if
reportErrorHandlerExceptionsToGlobalHandleristrue)
Does NOT emit when:
- ❌️
reportAllExceptionsis used (debug-only feature, not for production UI) - ❌️ Error is handled purely locally (
LocalErrorFilterwith local listeners) - ❌️ Error filter returns
ErrorReaction.noneorErrorReaction.throwException
Use Cases
1. Global Error Toasts (watch_it integration)
class MyApp extends WatchingWidget {
@override
Widget build(BuildContext context) {
registerStreamHandler<Stream<CommandError>, CommandError>(
target: Command.globalErrors,
handler: (context, snapshot, cancel) {
if (snapshot.hasData) {
final error = snapshot.data!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${error.error}')),
);
}
},
);
return MaterialApp(home: HomePage());
}
}2. Centralized Logging and Analytics
Use stream transformers to filter and route specific error types:
void setupErrorMonitoring() {
// Track only network errors for retry analytics
Command.globalErrors
.where((error) => error.error is NetworkException)
.listen((error) {
analytics.logEvent('network_error', parameters: {
'command': error.commandName ?? 'unknown',
'error_code': (error.error as NetworkException).statusCode,
});
});
// Log critical errors to crash reporter
Command.globalErrors
.where((error) => error.error is CriticalException)
.listen((error) {
crashReporter.logCritical(
error.error,
stackTrace: error.stackTrace,
command: error.commandName,
);
});
// General error metrics (all errors)
Command.globalErrors.listen((error) {
metrics.incrementCounter('command_errors_total');
metrics.recordErrorType(error.error.runtimeType.toString());
});
}Key Characteristics
- Broadcast stream: Multiple listeners supported
- Cannot be closed: Stream is managed by command_it, not user code
- Production-focused: Debug-only errors from
reportAllExceptionsare excluded - No null events emitted: Unlike
ValueListenable<CommandError?>, stream only emits actual errors
Relationship with globalExceptionHandler
Both receive the same errors, but serve different purposes:
| Feature | globalExceptionHandler | globalErrors |
|---|---|---|
| Type | Callback function | Stream |
| Purpose | Immediate error handling | Reactive error monitoring |
| Multiple handlers | No (single handler) | Yes (multiple listeners) |
watch_it integration | No | Yes (registerStreamHandler, watchStream) |
| Best for | Crash reporting, logging | UI notifications, analytics |
Typical Pattern: Use Both Together
Use globalExceptionHandler for immediate side effects like crash reporting and logging, while globalErrors stream is perfect for reactive UI updates using watch_it (registerStreamHandler or watchStream). This separation keeps your error handling clean and focused.
Exception Handling Workflow
The overall exception handling flow:
For the complete technical flow with all decision points, see the full exception handling diagram.
Key points:
- Mandatory checks: AssertionErrors and debug flags can bypass filtering
- ErrorFilter: Determines routing (local, global, throw, none)
- Local handlers: Listeners on
.errors/.resultsare called if configured - Global handler: Called based on ErrorReaction (emits to stream + calls callback)
- Handler exceptions: If error handler throws, can be routed to global handler with
originalError
Error Routing Patterns
Pattern 1: User vs System Errors
errorFilter: PredicatesErrorFilter([
// User-facing errors: show in UI
(error, _) => errorFilter<ValidationException>(
error,
ErrorReaction.localHandler,
),
(error, _) => errorFilter<AuthException>(
error,
ErrorReaction.localHandler,
),
// System errors: log and report
(error, _) => ErrorReaction.globalHandler,
])Pattern 2: Retry-able vs Fatal
errorFilter: PredicatesErrorFilter([
// Network timeouts: local handler with retry UI
(error, _) {
if (error is ApiException && error.statusCode == 408) {
return ErrorReaction.localHandler;
}
return null;
},
// Auth errors: global handler (centralized logout & scope cleanup)
(error, _) => errorFilter<AuthException>(
error,
ErrorReaction.globalHandler,
),
// Other: both handlers
(error, _) => ErrorReaction.localAndGlobalHandler,
])Pattern 3: Per-Command Configuration
class DataManager {
// Critical command: always report to global handler
late final saveCriticalData = Command.createAsync<Data, void>(
(data) => api.saveCritical(data),
errorFilter: const ErrorFilterConstant(ErrorReaction.globalHandler),
);
// Background sync: silent failure (don't bother user)
late final backgroundSync = Command.createAsyncNoParam<void>(
() => api.syncInBackground(),
errorFilter: const ErrorFilterConstant(ErrorReaction.none),
);
// Normal commands: use default (local then global)
late final fetchData = Command.createAsyncNoParam<List<Data>>(
() => api.fetch(),
initialValue: [],
// No errorFilter = uses Command.errorFilterDefault
);
}Optimistic Updates with Auto-Rollback
UndoableCommand provides automatic rollback on failure, perfect for optimistic UI updates. When an operation fails, the command automatically restores the previous state - no manual error recovery needed.
For complete details on implementing optimistic updates, automatic rollback, and manual undo/redo patterns, see Optimistic Updates.
Global Error Configuration
Configure error handling behavior globally in your main() function:
void main() {
// Default filter for all commands
Command.errorFilterDefault = const GlobalIfNoLocalErrorFilter();
// Global handler
Command.globalExceptionHandler = (error, stackTrace) {
loggingService.logError(error, stackTrace);
};
// AssertionErrors always throw (ignore filters)
Command.assertionsAlwaysThrow = true; // default
// Report ALL exceptions (override filters)
Command.reportAllExceptions = false; // default
// Report error handler exceptions to global handler
Command.reportErrorHandlerExceptionsToGlobalHandler = true; // default
// Capture detailed stack traces
Command.detailedStackTraces = true; // default
runApp(MyApp());
}errorFilterDefault
Default ErrorFilter used when no per-command filter is specified:
static ErrorFilter errorFilterDefault = const GlobalIfNoLocalErrorFilter();Default: GlobalIfNoLocalErrorFilter() - Smart routing that tries local handlers first, falls back to global
Use any of the predefined filters or define your own.
assertionsAlwaysThrow
AssertionErrors bypass all ErrorFilters and are always rethrown:
static bool assertionsAlwaysThrow = true; // defaultDefault: true (recommended)
Why this exists: AssertionErrors indicate programming mistakes (like assert(condition) failures). They should crash immediately during development to catch bugs, not be swallowed by error filters.
Recommendation: Keep this true to catch bugs early in development.
reportAllExceptions
Ensure every error calls globalExceptionHandler, regardless of ErrorFilter configuration:
static bool reportAllExceptions = false; // defaultDefault: false
How it works: When true, every error calls globalExceptionHandler immediately, in addition to normal ErrorFilter processing. ErrorFilters still run and control local handlers.
Common pattern - Debug vs Production:
// In main.dart
Command.reportAllExceptions = kDebugMode;What this does:
- Development: ALL errors reach global handler for visibility
- Production: Only errors routed by ErrorFilter reach global handler
When to use:
- Debugging error handling - ensure no errors are silently swallowed
- Development mode - see all errors regardless of ErrorFilter
- Verifying crash reporting - confirm all errors reach analytics
Potential Duplicate Calls
Command.reportAllExceptions = true;
Command.errorFilterDefault = const GlobalErrorFilter();
// Result: globalExceptionHandler called TWICE for each error!
// 1. From reportAllExceptions
// 2. From ErrorFilterIn production, use either reportAllExceptions OR ErrorFilters that call global, not both.
reportErrorHandlerExceptionsToGlobalHandler
Report exceptions thrown by error handlers to globalExceptionHandler:
static bool reportErrorHandlerExceptionsToGlobalHandler = true; // defaultDefault: true (recommended) - Error handlers can have bugs too; this prevents error handling code from crashing your app
See When Error Handlers Throw Exceptions for complete details, examples, and how it works.
detailedStackTraces
Clean up stack traces by filtering out framework noise:
static bool detailedStackTraces = true; // defaultDefault: true (recommended)
What it does: Uses the stack_trace package to filter and simplify stack traces.
Without detailedStackTraces - raw stack trace with 50+ lines of framework internals
With detailedStackTraces - filtered and simplified, showing only relevant frames
What gets filtered:
- Zone-related frames (async framework)
stack_tracepackage internalscommand_itinternal_runmethod frames
Performance: Stack trace processing has minimal overhead. Only disable if profiling shows it's a bottleneck (rare).
See Also
For non-error-related global configuration (like loggingHandler, useChainCapture), see Global Configuration.
Error Filters vs Try/Catch
❌️ Traditional approach:
Future<void> loadData() async {
try {
final data = await api.fetch();
// Handle success
} on ValidationException catch (e) {
// Show to user
} on ApiException catch (e) {
// Log to service
} catch (e) {
// Generic handler
}
}✅ With ErrorFilters:
late final loadCommand = Command.createAsyncNoParam<List<Data>>(
() => api.fetch(),
initialValue: [],
errorFilter: PredicatesErrorFilter([
(e, _) => errorFilter<ValidationException>(e, ErrorReaction.localHandler),
(e, _) => errorFilter<ApiException>(e, ErrorReaction.globalHandler),
(e, _) => ErrorReaction.localAndGlobalHandler,
]),
);
// Errors automatically routed
loadCommand.errors.listen((error, _) {
if (error != null) showErrorDialog(error.error.toString());
});Benefits:
- Declarative error routing
- Centralized handling logic
- Automatic UI updates via ValueListenable
- No scattered try/catch blocks
- Testable error routing
Debugging Error Handling
Enable detailed stack traces:
Command.detailedStackTraces = true;Log all error routing decisions:
Command.globalExceptionHandler = (error, stackTrace) {
debugPrint('Global handler: $error');
debugPrint('Stack: $stackTrace');
};
// In your predicates
PredicatesErrorFilter([
(error, stackTrace) {
debugPrint('Checking error: ${error.runtimeType}');
return errorFilter<ApiException>(error, ErrorReaction.localHandler);
},
])Test error scenarios:
// Force errors in development
final command = Command.createAsyncNoParam<Data>(
() async {
if (kDebugMode) {
throw ApiException('Test error');
}
return await api.fetch();
},
initialValue: Data.empty(),
errorFilter: yourFilter,
);Log all command errors:
command.errors.listen((error, _) {
if (error != null) {
debugPrint('''
Command Error:
- Error: ${error.error}
- Type: ${error.error.runtimeType}
- Param: ${error.paramData}
- Stack: ${error.stackTrace}
''');
}
});Find Missing Error Handlers During Development
Set the default error filter to throw while manually testing your app to catch unhandled errors immediately:
void main() {
// During development, make unhandled errors crash the app
if (kDebugMode) {
Command.errorFilterDefault = const ErrorFilterConstant(
ErrorReaction.throwException,
);
}
runApp(MyApp());
}What this does:
- Any command error without a local
.errorsor.resultslistener will throw - App crashes immediately, showing you exactly which command lacks error handling
- Forces you to add error handling before you can test that feature
- Only active in debug mode - production uses normal error routing
Example:
// Without error handling - app will crash when this fails
final loadData = Command.createAsyncNoParam<Data>(
() => api.fetch(),
initialValue: Data.empty(),
);
// ✅ Add error handling to prevent crash:
final loadData = Command.createAsyncNoParam<Data>(
() => api.fetch(),
initialValue: Data.empty(),
)..errors.listen((error, _) {
if (error != null) {
showErrorDialog(error.error.toString());
}
});Why this helps:
- Catches missing error handlers as soon as you trigger that code path
- Prevents shipping features without error handling
- Makes error handling a requirement, not an afterthought
- Remove
if (kDebugMode)check once all commands have handlers
TIP
This is a strict development mode. Once you've verified all commands have proper error handling, switch back to the default GlobalIfNoLocalErrorFilter() which provides better fallback behavior.
Common Mistakes
❌️ Forgetting to listen to .errors
// ErrorFilter uses localHandler but nothing listens
errorFilter: const LocalErrorFilter()
// Error: In debug mode, assertion thrown if no listeners❌️ Wrong order in PredicatesErrorFilter
// WRONG: General Exception before specific types
PredicatesErrorFilter([
(e, _) => errorFilter<Exception>(e, ErrorReaction.globalHandler),
(e, _) => errorFilter<ApiException>(e, ErrorReaction.localHandler), // Never reached!
])// CORRECT: Specific types first
PredicatesErrorFilter([
(e, _) => errorFilter<ApiException>(e, ErrorReaction.localHandler),
(e, _) => errorFilter<Exception>(e, ErrorReaction.globalHandler),
])❌️ Not handling cleared errors
Only Necessary If Using clearErrors()
This is only an issue if you explicitly call clearErrors(). By default, .errors never notifies with null, so you don't need null checks.
// If you use clearErrors(), handle null:
command.errors.listen((error, _) {
if (error != null) {
showErrorDialog(error.error.toString());
}
});See Also
- Command Properties — The
.errorsproperty - Command Results — Using errors with CommandResult
- Command Basics — Creating commands
- Command Types — Error filter parameters
- Best Practices — Production error handling patterns