Perché la gestione degli errori merita una strategia
In ogni app reale gli errori sono inevitabili: chiamate di rete che falliscono, parsing JSON malformato, eccezioni inattese nel codice. La differenza tra un'app amatoriale e una professionale sta nel modo in cui questi errori vengono catturati, comunicati all'utente e monitorati in produzione.
In questo articolo vedremo tre livelli di gestione:
- Gli error handler globali di Flutter per intercettare tutto.
- Il pattern Result per rappresentare in modo esplicito gli errori prevedibili.
- Il crash reporting in produzione con Sentry.
Gli error handler globali
Flutter offre diversi punti di aggancio per catturare gli errori non gestiti. È buona norma configurarli nel main.
void main() {
runZonedGuarded(() {
// Errori del framework (build, layout, ecc.)
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.presentError(details);
logError(details.exception, details.stack);
};
// Errori a basso livello della piattaforma
PlatformDispatcher.instance.onError = (error, stack) {
logError(error, stack);
return true;
};
runApp(const MyApp());
}, (error, stack) {
// Errori asincroni non catturati
logError(error, stack);
});
}
Questi tre canali insieme coprono la stragrande maggioranza degli errori: quelli del framework, quelli nativi e quelli asincroni fuori dalla zone.
Il pattern Result per errori prevedibili
Lanciare eccezioni per errori attesi (come una rete offline) rende il flusso difficile da seguire. Il pattern Result rende esplicito, nel tipo di ritorno, che un'operazione può fallire.
Da Dart 3 possiamo usare i sealed class e il pattern matching per un'implementazione elegante.
sealed class Result<T> {
const Result();
}
final class Success<T> extends Result<T> {
final T value;
const Success(this.value);
}
final class Failure<T> extends Result<T> {
final Object error;
final StackTrace? stackTrace;
const Failure(this.error, [this.stackTrace]);
}
Un repository può quindi restituire un Result invece di lanciare eccezioni:
Future<Result<User>> fetchUser(String id) async {
try {
final response = await dio.get('/users/$id');
return Success(User.fromJson(response.data));
} catch (e, stack) {
return Failure(e, stack);
}
}
Nel layer di presentazione il consumo è chiaro e obbliga il compilatore a gestire entrambi i casi:
final result = await repository.fetchUser('42');
switch (result) {
case Success(:final value):
emit(UserLoaded(value));
case Failure(:final error):
emit(UserError(_mapError(error)));
}
Convertire gli errori in messaggi utente
Gli utenti non devono mai vedere un'eccezione grezza. Mappa gli errori tecnici in messaggi comprensibili:
String _mapError(Object error) {
return switch (error) {
DioException(type: DioExceptionType.connectionError) =>
'Nessuna connessione a Internet.',
DioException(response: final r?) when r.statusCode == 404 =>
'Risorsa non trovata.',
_ => 'Si è verificato un errore imprevisto.',
};
}
Crash reporting in produzione con Sentry
Gli error handler intercettano gli errori localmente, ma in produzione hai bisogno di sapere cosa succede sui dispositivi degli utenti. Sentry è tra le soluzioni più usate.
Aggiungi la dipendenza:
dependencies:
sentry_flutter: ^8.0.0
Sentry semplifica l'inizializzazione gestendo internamente gli handler globali:
Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn = 'https://esempio@sentry.io/123';
options.tracesSampleRate = 0.2; // performance monitoring
options.environment = kReleaseMode ? 'production' : 'development';
},
appRunner: () => runApp(const MyApp()),
);
}
Inviare errori catturati manualmente
Per gli errori che gestisci con il pattern Result puoi comunque loggarli su Sentry senza interrompere l'app:
void logError(Object error, StackTrace? stack) {
Sentry.captureException(error, stackTrace: stack);
}
Aggiungere contesto
Un crash isolato dice poco. Aggiungi breadcrumb e informazioni sull'utente per una diagnosi più rapida:
Sentry.configureScope((scope) {
scope.setUser(SentryUser(id: user.id));
scope.setTag('feature', 'checkout');
});
Sentry.addBreadcrumb(
Breadcrumb(message: 'Utente ha aperto il carrello'),
);
Best practice riepilogative
- Non ignorare mai gli errori con un
catchvuoto: almeno loggali. - Usa il pattern Result per gli errori attesi e le eccezioni solo per condizioni davvero inaspettate.
- Configura tutti e tre gli handler globali (
FlutterError.onError,PlatformDispatcher.onError,runZonedGuarded). - Attiva il crash reporting solo in release o distingui gli ambienti con
environment. - Non loggare dati sensibili: filtra le informazioni prima di inviarle a servizi esterni.
- Mostra all'utente messaggi chiari, non stack trace.
Conclusione
Una strategia di gestione degli errori a più livelli rende la tua app Flutter più robusta e manutenibile. Il pattern Result porta chiarezza nel codice, gli handler globali fanno da rete di sicurezza e strumenti come Sentry ti danno visibilità su ciò che accade dopo il rilascio. Investire in questa infrastruttura fin dall'inizio ripaga in ogni fase del ciclo di vita del prodotto.
