Manejo de Errores (Error Handling)
Deja de preocuparte por excepciones no capturadas que crashean tu app. command_it proporciona manejo automático de excepciones con potentes capacidades de enrutamiento - no más bloques try-catch desordenados o tipos Result<T, Error> en todas partes.
Características Clave:
- 🛡️ Nunca te preocupes por excepciones - Los Commands capturan todos los errores automáticamente
- 🎯 Potente enrutamiento de errores - Enruta errores localmente, globalmente, o déjalos lanzar
- 🎁 Deja de retornar tipos Result - Las funciones retornan
Tlimpio, noResult<T, Error> - 📡 Manejo de errores reactivo -
Streams yValueListenableobservables para errores - 🔧 Filtros flexibles - Configura estrategias de manejo de errores por command o globalmente
Desde escucha básica de errores hasta patrones avanzados de enrutamiento, command_it te da control completo sobre cómo tu app maneja fallos.
¡No Te Intimides!
Esta documentación es comprehensiva, pero el manejo de errores en command_it es realmente simple una vez que entiendes el principio central: los errores son solo datos que fluyen por tu app. Comienza con Manejo Básico de Errores abajo - puedes escuchar .errors igual que cualquier otra propiedad.
Manejo Básico de Errores
Si la función envuelta dentro de un Command lanza una excepción, el command la captura para que tu app no crashee. En su lugar, envuelve el error capturado junto con el valor del parámetro en un objeto CommandError y lo asigna a la propiedad .errors del command.
La Propiedad .errors
Los Commands exponen una propiedad .errors de tipo ValueListenable<CommandError?>:
Comportamiento:
.errorsse resetea anullal inicio de la ejecución (no notifica a listeners).errorsse establece aCommandError<TParam>en fallo (notifica a listeners)CommandErrorcontiene:error,paramData,stackTrace
Patrón 1: Mostrar Estado de Error con watchValue
Observa el valor del error para mostrarlo en tu 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('Sin errores');
}
}Patrón 2: Manejar Errores con registerHandler
Usa registerHandler para efectos secundarios como mostrar toasts o snackbars:
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Mostrar snackbar con botón de reintentar cuando ocurre error
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: 'Reintentar',
onPressed: () => di<TodoManager>().loadTodos(error.paramData),
),
),
);
}
},
);
return TodoList();
}
}Patrón 3: Escuchar Directamente en la Definición del Command
Encadena .listen() al definir commands para logging o analytics:
class DataManager {
late final loadData = Command.createAsyncNoParam<List<Item>>(
() => api.fetchData(),
[],
)..errors.listen((error, _) {
if (error != null) {
debugPrint('Carga falló: ${error.error}');
analytics.logError(error.error, error.stackTrace);
}
});
}Estos patrones se denominan manejo local de errores porque manejan errores para un command específico. Esto te da control granular sobre cómo se manejan los errores de cada command. Para manejar errores de múltiples commands en un solo lugar, ver Handler de Error Global abajo.
Comportamiento de Limpieza de Errores
La propiedad .errors normalmente nunca notifica con un valor null a menos que explícitamente llames clearErrors(). Normalmente nunca necesitas llamar clearErrors() - y si no lo haces, no necesitas agregar verificaciones if (error != null) en tus handlers de error. Ver clearErrors para detalles.
Sin watch_it
Para patrones con StatefulWidget usando .listen() en initState, ver Sin watch_it para patrones.
Usando CommandResult
También puedes acceder a errores a través de .results que combina todo el estado del command:
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),
);
}
// ... manejar otros estados
}
}Ver Command Results para detalles.
Handler de Error Global
Establece un handler de error global para capturar todos los errores de commands enrutados por ErrorFilter:
static void Function(CommandError<dynamic> error, StackTrace stackTrace)?
globalExceptionHandler;Acceso al Contexto del Error
CommandError<TParam> proporciona contexto rico:
.error- La excepción real lanzada.commandName- Nombre/identificador del command (desdedebugName).paramData- Parámetro pasado al command.stackTrace- Stack trace completo.errorReaction- Cómo se manejó el error
Manejando Tipos de Error Específicos
Puedes manejar diferentes tipos de error centralmente en tu handler global. Un patrón común es manejar errores de autenticación haciendo logout y limpiando scopes:
void setupGlobalExceptionHandler() {
Command.globalExceptionHandler = (commandError, stackTrace) {
final error = commandError.error;
// Manejar errores de auth: logout y limpiar
if (error is AuthException) {
// Logout (limpia tokens, estado de usuario, etc.)
getIt<UserManager>().logout();
// Pop del scope 'loggedIn' para disponer servicios de sesión
// Ver: https://flutter-it.dev/documentation/get_it/scopes
getIt.popScope();
// Navegación a pantalla de login ocurriría via observer de nivel de app
// observando userManager.isLoggedIn
return;
}
// Manejar otros errores: log a crash reporter
crashReporter.logError(error, stackTrace);
};
}Esto centraliza la limpieza de autenticación - cualquier command que lance AuthException automáticamente disparará logout, sin importar dónde se llame.
Uso con 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');
}
};
}Cuándo se llama al handler global depende de tu configuración de ErrorFilter. Ver Filtros Incorporados para detalles.
Filtros de Error
Los filtros de error deciden cómo debe manejarse cada error: por un handler local, el handler global, ambos, o ninguno. En lugar de tratar todos los errores igual, puedes enrutarlos declarativamente basándote en tipo o condiciones.
¿Por Qué Usar Filtros de Error?
Diferentes tipos de error necesitan diferente manejo:
- Errores de validación → Mostrar al usuario en UI
- Errores de red → Lógica de reintento o modo offline
- Errores de autenticación → Redirigir a login
- Errores críticos → Log a servicio de monitoreo
- Todo sin bloques try/catch dispersos
Dos Enfoques para Filtrado de Errores
Los Commands soportan dos formas mutuamente exclusivas de especificar lógica de filtrado de errores:
Enfoque basado en función (errorFilterFn) - Función directa con type-safety en tiempo de compilación:
typedef ErrorFilterFn = ErrorReaction? Function(
Object error,
StackTrace stackTrace,
);
Command.createAsync(
fetchData,
[],
errorFilterFn: (e, s) => e is NetworkException
? ErrorReaction.globalHandler
: null,
// ¡Firma verificada en tiempo de compilación! ✅
);Enfoque basado en clase (errorFilter) - Objetos ErrorFilter para lógica compleja:
Command.createAsync(
fetchData,
[],
errorFilter: PredicatesErrorFilter([
(e, s) => e is NetworkException ? ErrorReaction.globalHandler : null,
(e, s) => e is ValidationException ? ErrorReaction.localHandler : null,
]),
);command_it proporciona clases de filtro incorporadas (PredicatesErrorFilter, TableErrorFilter, etc.), pero también puedes definir las tuyas propias implementando la interface ErrorFilter.
Diferencias clave:
| Característica | errorFilterFn (Función) | errorFilter (Clase) |
|---|---|---|
| Simplicidad | ✅ Función inline directa | Requiere creación de objeto |
| Con parámetros | ❌️ Necesita wrapper lambda | ✅ Puede ser objetos const |
| Reutilización | ❌️ Crea nuevo closure cada vez | ✅ Reutiliza misma instancia const |
| Mejor para | Filtros simples, únicos | Filtros parametrizados, reutilizables |
Mutuamente Exclusivos
No puedes usar tanto errorFilter como errorFilterFn en el mismo command - un assertion lo impide. Elige un enfoque basándote en tus necesidades.
Enum ErrorReaction
Un ErrorFilter retorna un ErrorReaction para especificar el manejo:
| Reacción | Comportamiento |
|---|---|
| localHandler | Llama listeners en .errors/.results |
| globalHandler | Llama Command.globalExceptionHandler |
| localAndGlobalHandler | Llama ambos handlers |
| firstLocalThenGlobalHandler | Intenta local, fallback a global (por defecto) |
| throwException | Relanza inmediatamente (solo debugging) |
| throwIfNoLocalHandler | Lanza si no hay listeners |
| noHandlersThrowException | Lanza si no hay handlers presentes |
| none | Silencia sin hacer nada |
Filtros de Error Simples
Filtros const incorporados para patrones de enrutamiento comunes:
| Filtro | Comportamiento | Uso |
|---|---|---|
| ErrorFilterConstant | Siempre retorna mismo ErrorReaction | const ErrorFilterConstant(ErrorReaction.none) |
| LocalErrorFilter | Enruta solo a handler local | const LocalErrorFilter() |
| GlobalIfNoLocalErrorFilter | Intenta local, fallback a global (por defecto) | const GlobalIfNoLocalErrorFilter() |
| LocalAndGlobalErrorFilter | Enruta a ambos handlers local y global | const LocalAndGlobalErrorFilter() |
Ejemplo:
// Fallo silencioso para sync en background
late final backgroundSync = Command.createAsyncNoParam<void>(
() => api.syncInBackground(),
errorFilter: const ErrorFilterConstant(ErrorReaction.none),
);
// Debug: lanzar en error para capturar en debugger
late final debugCommand = Command.createAsync<Data, void>(
(data) => api.saveCritical(data),
errorFilter: const ErrorFilterConstant(ErrorReaction.throwException),
);Entendiendo GlobalIfNoLocalErrorFilter (El Por Defecto)
Por qué es el por defecto: El GlobalIfNoLocalErrorFilter proporciona enrutamiento inteligente que se adapta a tu código. Retorna firstLocalThenGlobalHandler, que funciona así:
Cómo funciona:
- Verifica si existen listeners locales - ¿Estás manejando
.errorso.resultspara este command (listen, watchValue, registerHandler)? - Si SÍ → Enruta solo a handler local (asume que lo estás manejando)
- Si NO → Fallback a handler global (previene fallos silenciosos)
Por qué importa:
// Ejemplo 1: Tiene handler de error local
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Existe listener local
final error = watchValue((TodoManager m) => m.loadTodos.errors);
// ✅ Errores enrutan SOLO a handler LOCAL
if (error != null) return ErrorWidget(error);
return TodoList();
}
}
// Ejemplo 2: Sin handler de error local
class DataManager {
late final loadData = Command.createAsyncNoParam<List<Item>>(
() => api.fetchData(),
[],
);
// ❌ Sin listeners de .errors/.results
// ✅ Errores enrutan a handler GLOBAL automáticamente
}Esto previene el error común de olvidar manejar errores - al menos llegarán a tu crash reporter global. Si agregas un handler local después, el handler global automáticamente deja de ser llamado para ese command.
Ver el diagrama de flujo de manejo de excepciones para el flujo de decisión completo.
ErrorFilter Personalizado
Construye tus propios ErrorFilters para enrutamiento avanzado:
// Manejar errores de cliente 4xx localmente, dejar 5xx ir a handler global
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;
}
}Más Filtros de Error
PredicatesErrorFilter (Recomendado)
Encadena predicados para coincidir errores por jerarquía de tipos:
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),
),
],
),
);
}
}Cómo funciona:
- Los predicados son funciones:
(error, stackTrace) => ErrorReaction? - Retorna la primera reacción no-null
- Fallback a defecto si ninguno coincide
- El orden importa - verifica tipos específicos primero
Patrón:
PredicatesErrorFilter([
(error, stackTrace) => errorFilter<ApiException>(
error,
ErrorReaction.localHandler,
),
(error, stackTrace) => errorFilter<ValidationException>(
error,
ErrorReaction.localHandler,
),
(error, stackTrace) => ErrorReaction.globalHandler, // Por defecto
])Prefiere esto para la mayoría de casos - es más flexible que TableErrorFilter.
TableErrorFilter
Mapea tipos de error a reacciones usando igualdad de tipo exacta:
errorFilter: TableErrorFilter({
ApiException: ErrorReaction.localHandler,
ValidationException: ErrorReaction.localHandler,
NetworkException: ErrorReaction.globalHandler,
Exception: ErrorReaction.globalHandler,
})Limitaciones:
- Solo coincide tipo de runtime exacto (no jerarquía de tipos)
- No puede distinguir subclases
- Workaround especial para tipo
Exception
Cuándo usar:
- Enrutamiento simple de errores por tipo
- Conjunto conocido de tipos de error
- Sin jerarquías de herencia
Comportamiento de Errores con runAsync()
Cuando usas runAsync() y el command lanza una excepción, ambas cosas suceden:
- Se llaman los handlers de error - Los listeners de
.errorsyglobalExceptionHandlerreciben el error (basado en ErrorFilter) - El Future completa con error - La excepción se relanza al caller
Importante: DEBES envolver runAsync() en try/catch para prevenir crashes de la app:
// ✅ BIEN: Captura la excepción relanzada
try {
final result = await loadCommand.runAsync();
// Usa result...
} catch (e) {
// Maneja el error - muestra feedback de UI, log, etc.
showErrorToast(e.toString());
}
// ❌ MAL: Excepción no manejada crasheará la app
await loadCommand.runAsync(); // ¡Si esto lanza, la app crashea!Usando ambos try/catch y listener de .errors:
Si tienes un listener de .errors para actualizaciones reactivas de UI, aún necesitas try/catch pero el bloque catch puede estar vacío:
// Configura listener de error para UI reactiva
loadCommand.errors.listen((error, _) {
if (error != null) showErrorToast(error.error);
});
// Aún necesita try/catch para prevenir crash
try {
final result = await loadCommand.runAsync();
// Usa result...
} catch (e) {
// Error ya manejado por listener de .errors arriba
// Catch vacío solo previene el crash
}ErrorReaction.none No Permitido
Usar ErrorReaction.none con runAsync() disparará un error de assertion. Como el error sería silenciado, no hay valor con el que completar el Future.
Cuando los Handlers de Error Lanzan Excepciones
Los handlers de error son código Dart regular - pueden fallar también. Cuando tu handler de error hace llamadas API async o procesa datos, esas operaciones pueden lanzar excepciones.
El Problema
Los handlers de error que realizan efectos secundarios pueden fallar en muchos escenarios:
- ⚠️ Operaciones async - Logging a servicios remotos que podrían hacer timeout
- ⚠️ Procesamiento de datos - Errores de parsing o formateo
Sin manejo apropiado, estas excepciones secundarias podrían crashear tu app o pasar desapercibidas.
reportErrorHandlerExceptionsToGlobalHandler
Controla si las excepciones lanzadas dentro de handlers de error se reportan al handler global:
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;
}Configuración:
// En main() o inicialización de app (esto es el por defecto)
Command.reportErrorHandlerExceptionsToGlobalHandler = true;Ver Configuración Global - reportErrorHandlerExceptionsToGlobalHandler para detalles.
Cómo Funciona
Con true (por defecto, recomendado):
- ✅ Las excepciones en handlers de error se capturan automáticamente
- ✅ Se envían a
Command.globalExceptionHandler - ✅ El error original del command se preserva en
CommandError.originalError - ✅ Tu app no crashea por código buggy de manejo de errores
Con false:
- ❌️ Solo se loguea por el logger de errores de Flutter
- ❌️ No llegará a tu handler de excepciones global
- ❌️ Menos visibilidad de bugs en handlers de error
Cómo Funciona Esto
La propiedad .errors es un CustomValueNotifier de listen_it, que proporciona la capacidad incorporada de capturar excepciones lanzadas por listeners. Puedes usar esta misma característica en tu propio código con CustomValueNotifier - ver listen_it CustomValueNotifier para detalles.
Recomendación de Producción
Siempre mantén reportErrorHandlerExceptionsToGlobalHandler: true en producción. Los fallos de handlers de error indican bugs en tu código de manejo de errores que necesitan atención inmediata.
Stream de Errores Globales
Stream estático en la clase Command para todos los errores de commands enrutados al handler global:
static Stream<CommandError<dynamic>> get globalErrorsResumen
Un stream de broadcast que emite CommandError<dynamic> para cada error que dispararía globalExceptionHandler. Perfecto para monitoreo centralizado de errores, analytics, crash reporting, y notificaciones de UI globales.
Comportamiento del Stream
Emite cuando:
- ✅️
ErrorFilterenruta error a handler global (basado en configuración del filtro) - ✅️ El handler de error mismo lanza una excepción (si
reportErrorHandlerExceptionsToGlobalHandlerestrue)
NO emite cuando:
- ❌️ Se usa
reportAllExceptions(característica solo de debug, no para UI de producción) - ❌️ El error se maneja puramente localmente (
LocalErrorFiltercon listeners locales) - ❌️ El filtro de error retorna
ErrorReaction.noneoErrorReaction.throwException
Casos de Uso
1. Toasts de Error Globales (integración con watch_it)
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. Logging Centralizado y Analytics
Usa transformadores de stream para filtrar y enrutar tipos de error específicos:
void setupErrorMonitoring() {
// Rastrear solo errores de red para analytics de reintento
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 de errores críticos a crash reporter
Command.globalErrors
.where((error) => error.error is CriticalException)
.listen((error) {
crashReporter.logCritical(
error.error,
stackTrace: error.stackTrace,
command: error.commandName,
);
});
// Métricas generales de error (todos los errores)
Command.globalErrors.listen((error) {
metrics.incrementCounter('command_errors_total');
metrics.recordErrorType(error.error.runtimeType.toString());
});
}Características Clave
- Stream de broadcast: Soporta múltiples listeners
- No puede cerrarse: El stream es gestionado por command_it, no por código de usuario
- Enfocado en producción: Errores solo de debug de
reportAllExceptionsestán excluidos - No emite eventos null: A diferencia de
ValueListenable<CommandError?>, el stream solo emite errores reales
Relación con globalExceptionHandler
Ambos reciben los mismos errores, pero sirven propósitos diferentes:
| Característica | globalExceptionHandler | globalErrors |
|---|---|---|
| Tipo | Función callback | Stream |
| Propósito | Manejo inmediato de errores | Monitoreo reactivo de errores |
| Múltiples handlers | No (handler único) | Sí (múltiples listeners) |
Integración con watch_it | No | Sí (registerStreamHandler, watchStream) |
| Mejor para | Crash reporting, logging | Notificaciones de UI, analytics |
Patrón Típico: Usar Ambos Juntos
Usa globalExceptionHandler para efectos secundarios inmediatos como crash reporting y logging, mientras que el stream globalErrors es perfecto para actualizaciones de UI reactivas usando watch_it (registerStreamHandler o watchStream). Esta separación mantiene tu manejo de errores limpio y enfocado.
Flujo de Trabajo de Manejo de Excepciones
El flujo general de manejo de excepciones:
Para el flujo técnico completo con todos los puntos de decisión, ver el diagrama completo de manejo de excepciones.
Puntos clave:
- Verificaciones obligatorias: AssertionErrors y flags de debug pueden bypasear filtrado
- ErrorFilter: Determina enrutamiento (local, global, throw, none)
- Handlers locales: Listeners en
.errors/.resultsse llaman si están configurados - Handler global: Se llama basándose en ErrorReaction (emite a stream + llama callback)
- Excepciones de handler: Si el handler de error lanza, puede enrutarse a handler global con
originalError
Patrones de Enrutamiento de Errores
Patrón 1: Errores de Usuario vs Sistema
errorFilter: PredicatesErrorFilter([
// Errores para el usuario: mostrar en UI
(error, _) => errorFilter<ValidationException>(
error,
ErrorReaction.localHandler,
),
(error, _) => errorFilter<AuthException>(
error,
ErrorReaction.localHandler,
),
// Errores de sistema: log y reportar
(error, _) => ErrorReaction.globalHandler,
])Patrón 2: Reintentable vs Fatal
errorFilter: PredicatesErrorFilter([
// Timeouts de red: handler local con UI de reintento
(error, _) {
if (error is ApiException && error.statusCode == 408) {
return ErrorReaction.localHandler;
}
return null;
},
// Errores de auth: handler global (logout centralizado & limpieza de scope)
(error, _) => errorFilter<AuthException>(
error,
ErrorReaction.globalHandler,
),
// Otros: ambos handlers
(error, _) => ErrorReaction.localAndGlobalHandler,
])Patrón 3: Configuración Por Command
class DataManager {
// Command crítico: siempre reportar a handler global
late final saveCriticalData = Command.createAsync<Data, void>(
(data) => api.saveCritical(data),
errorFilter: const ErrorFilterConstant(ErrorReaction.globalHandler),
);
// Sync en background: fallo silencioso (no molestar al usuario)
late final backgroundSync = Command.createAsyncNoParam<void>(
() => api.syncInBackground(),
errorFilter: const ErrorFilterConstant(ErrorReaction.none),
);
// Commands normales: usar por defecto (local luego global)
late final fetchData = Command.createAsyncNoParam<List<Data>>(
() => api.fetch(),
initialValue: [],
// Sin errorFilter = usa Command.errorFilterDefault
);
}Actualizaciones Optimistas con Auto-Rollback
UndoableCommand proporciona rollback automático en fallo, perfecto para actualizaciones optimistas de UI. Cuando una operación falla, el command automáticamente restaura el estado anterior - no se necesita recuperación manual de errores.
Para detalles completos sobre implementar actualizaciones optimistas, rollback automático, y patrones de undo/redo manual, ver Actualizaciones Optimistas.
Configuración Global de Errores
Configura el comportamiento de manejo de errores globalmente en tu función main():
void main() {
// Filtro por defecto para todos los commands
Command.errorFilterDefault = const GlobalIfNoLocalErrorFilter();
// Handler global
Command.globalExceptionHandler = (error, stackTrace) {
loggingService.logError(error, stackTrace);
};
// AssertionErrors siempre lanzan (ignoran filtros)
Command.assertionsAlwaysThrow = true; // por defecto
// Reportar TODAS las excepciones (sobrescribir filtros)
Command.reportAllExceptions = false; // por defecto
// Reportar excepciones de handler de error a handler global
Command.reportErrorHandlerExceptionsToGlobalHandler = true; // por defecto
// Capturar stack traces detallados
Command.detailedStackTraces = true; // por defecto
runApp(MyApp());
}errorFilterDefault
ErrorFilter por defecto usado cuando no se especifica filtro por command:
static ErrorFilter errorFilterDefault = const GlobalIfNoLocalErrorFilter();Por defecto: GlobalIfNoLocalErrorFilter() - Enrutamiento inteligente que intenta handlers locales primero, fallback a global
Usa cualquiera de los filtros predefinidos o define el tuyo propio.
assertionsAlwaysThrow
AssertionErrors bypasean todos los ErrorFilters y siempre se relanzan:
static bool assertionsAlwaysThrow = true; // por defectoPor defecto: true (recomendado)
Por qué existe: AssertionErrors indican errores de programación (como fallos de assert(condition)). Deberían crashear inmediatamente durante desarrollo para capturar bugs, no ser silenciados por filtros de error.
Recomendación: Mantén esto true para capturar bugs temprano en desarrollo.
reportAllExceptions
Asegura que cada error llame a globalExceptionHandler, sin importar la configuración de ErrorFilter:
static bool reportAllExceptions = false; // por defectoPor defecto: false
Cómo funciona: Cuando es true, cada error llama a globalExceptionHandler inmediatamente, además del procesamiento normal de ErrorFilter. Los ErrorFilters aún se ejecutan y controlan handlers locales.
Patrón común - Debug vs Producción:
// En main.dart
Command.reportAllExceptions = kDebugMode;Qué hace esto:
- Desarrollo: TODOS los errores llegan a handler global para visibilidad
- Producción: Solo errores enrutados por ErrorFilter llegan a handler global
Cuándo usar:
- Debugging de manejo de errores - asegurar que ningún error se silencia
- Modo desarrollo - ver todos los errores sin importar ErrorFilter
- Verificar crash reporting - confirmar que todos los errores llegan a analytics
Potenciales Llamadas Duplicadas
Command.reportAllExceptions = true;
Command.errorFilterDefault = const GlobalErrorFilter();
// Resultado: ¡globalExceptionHandler llamado DOS VECES por cada error!
// 1. Desde reportAllExceptions
// 2. Desde ErrorFilterEn producción, usa reportAllExceptions O ErrorFilters que llamen a global, no ambos.
reportErrorHandlerExceptionsToGlobalHandler
Reporta excepciones lanzadas por handlers de error a globalExceptionHandler:
static bool reportErrorHandlerExceptionsToGlobalHandler = true; // por defectoPor defecto: true (recomendado) - Los handlers de error también pueden tener bugs; esto previene que código de manejo de errores crashee tu app
Ver Cuando los Handlers de Error Lanzan Excepciones para detalles completos, ejemplos, y cómo funciona.
detailedStackTraces
Limpia stack traces filtrando ruido de framework:
static bool detailedStackTraces = true; // por defectoPor defecto: true (recomendado)
Qué hace: Usa el paquete stack_trace para filtrar y simplificar stack traces.
Sin detailedStackTraces - stack trace crudo con 50+ líneas de internos de framework
Con detailedStackTraces - filtrado y simplificado, mostrando solo frames relevantes
Qué se filtra:
- Frames relacionados con Zone (framework async)
- Internos del paquete
stack_trace - Frames del método interno
_runde command_it
Rendimiento: El procesamiento de stack trace tiene overhead mínimo. Solo deshabilita si profiling muestra que es un cuello de botella (raro).
Ver También
Para configuración global no relacionada con errores (como loggingHandler, useChainCapture), ver Configuración Global.
Filtros de Error vs Try/Catch
❌️ Enfoque tradicional:
Future<void> loadData() async {
try {
final data = await api.fetch();
// Manejar éxito
} on ValidationException catch (e) {
// Mostrar al usuario
} on ApiException catch (e) {
// Log a servicio
} catch (e) {
// Handler genérico
}
}✅ Con 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,
]),
);
// Errores enrutados automáticamente
loadCommand.errors.listen((error, _) {
if (error != null) showErrorDialog(error.error.toString());
});Beneficios:
- Enrutamiento declarativo de errores
- Lógica de manejo centralizada
- Actualizaciones de UI automáticas via ValueListenable
- Sin bloques try/catch dispersos
- Enrutamiento de errores testeable
Debugging de Manejo de Errores
Habilitar stack traces detallados:
Command.detailedStackTraces = true;Log de todas las decisiones de enrutamiento de errores:
Command.globalExceptionHandler = (error, stackTrace) {
debugPrint('Handler global: $error');
debugPrint('Stack: $stackTrace');
};
// En tus predicados
PredicatesErrorFilter([
(error, stackTrace) {
debugPrint('Verificando error: ${error.runtimeType}');
return errorFilter<ApiException>(error, ErrorReaction.localHandler);
},
])Probar escenarios de error:
// Forzar errores en desarrollo
final command = Command.createAsyncNoParam<Data>(
() async {
if (kDebugMode) {
throw ApiException('Error de prueba');
}
return await api.fetch();
},
initialValue: Data.empty(),
errorFilter: yourFilter,
);Log de todos los errores de command:
command.errors.listen((error, _) {
if (error != null) {
debugPrint('''
Error de Command:
- Error: ${error.error}
- Tipo: ${error.error.runtimeType}
- Param: ${error.paramData}
- Stack: ${error.stackTrace}
''');
}
});Encontrar Handlers de Error Faltantes Durante Desarrollo
Establece el filtro de error por defecto para lanzar mientras pruebas manualmente tu app para capturar errores no manejados inmediatamente:
void main() {
// Durante desarrollo, hacer que errores no manejados crasheen la app
if (kDebugMode) {
Command.errorFilterDefault = const ErrorFilterConstant(
ErrorReaction.throwException,
);
}
runApp(MyApp());
}Qué hace esto:
- Cualquier error de command sin listener local de
.errorso.resultslanzará - La app crashea inmediatamente, mostrándote exactamente qué command carece de manejo de errores
- Te fuerza a agregar manejo de errores antes de poder probar esa característica
- Solo activo en modo debug - producción usa enrutamiento normal de errores
Ejemplo:
// Sin manejo de errores - la app crasheará cuando esto falle
final loadData = Command.createAsyncNoParam<Data>(
() => api.fetch(),
initialValue: Data.empty(),
);
// ✅ Agrega manejo de errores para prevenir crash:
final loadData = Command.createAsyncNoParam<Data>(
() => api.fetch(),
initialValue: Data.empty(),
)..errors.listen((error, _) {
if (error != null) {
showErrorDialog(error.error.toString());
}
});Por qué esto ayuda:
- Captura handlers de error faltantes tan pronto como disparas ese path de código
- Previene enviar características sin manejo de errores
- Hace el manejo de errores un requisito, no algo para después
- Remueve la verificación
if (kDebugMode)una vez que todos los commands tienen handlers
TIP
Este es un modo de desarrollo estricto. Una vez que hayas verificado que todos los commands tienen manejo de errores apropiado, cambia de vuelta al GlobalIfNoLocalErrorFilter() por defecto que proporciona mejor comportamiento de fallback.
Errores Comunes
❌️ Olvidar escuchar .errors
// ErrorFilter usa localHandler pero nada escucha
errorFilter: const LocalErrorFilter()
// Error: En modo debug, assertion lanza si no hay listeners❌️ Orden incorrecto en PredicatesErrorFilter
// MAL: Exception general antes de tipos específicos
PredicatesErrorFilter([
(e, _) => errorFilter<Exception>(e, ErrorReaction.globalHandler),
(e, _) => errorFilter<ApiException>(e, ErrorReaction.localHandler), // ¡Nunca se alcanza!
])// CORRECTO: Tipos específicos primero
PredicatesErrorFilter([
(e, _) => errorFilter<ApiException>(e, ErrorReaction.localHandler),
(e, _) => errorFilter<Exception>(e, ErrorReaction.globalHandler),
])❌️ No manejar errores limpiados
Solo Necesario Si Usas clearErrors()
Esto solo es un problema si explícitamente llamas clearErrors(). Por defecto, .errors nunca notifica con null, así que no necesitas verificaciones de null.
// Si usas clearErrors(), maneja null:
command.errors.listen((error, _) {
if (error != null) {
showErrorDialog(error.error.toString());
}
});Ver También
- Propiedades del Command — La propiedad
.errors - Command Results — Usando errores con CommandResult
- Fundamentos de Command — Creando commands
- Tipos de Command — Parámetros de filtro de error
- Mejores Prácticas — Patrones de manejo de errores en producción