Catch uncaught async errors and surface response-shape failures

Wrap main in runZonedGuarded and install FlutterError.onError /
PlatformDispatcher.onError so background async failures get logged
instead of dying silently. Guard PrefUtil.init so a SharedPreferences
platform error no longer aborts startup. Recognize TypeError and
FormatException in dioErrorToUserMessage to give a clearer message
when the backend returns an unexpected JSON shape or bad timestamp.
Wrap Settings._save in try/catch so prefs write failures show a
snackbar instead of leaving the Save button silently dead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-31 18:55:35 -04:00
co-authored by Claude Sonnet 4.6
parent e70676a193
commit 140ebab035
3 changed files with 55 additions and 18 deletions
+26 -1
View File
@@ -1,3 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:frontend/theme/catppuccin_mocha_theme.dart'; import 'package:frontend/theme/catppuccin_mocha_theme.dart';
import 'package:frontend/utils/pref_utils.dart'; import 'package:frontend/utils/pref_utils.dart';
@@ -5,10 +8,32 @@ import 'package:provider/provider.dart';
import 'navigation.dart'; import 'navigation.dart';
import 'theme/gruvbox_theme.dart'; import 'theme/gruvbox_theme.dart';
void main() async { void main() {
runZonedGuarded(
() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (details) {
FlutterError.presentError(details);
debugPrint('FlutterError: ${details.exceptionAsString()}');
};
PlatformDispatcher.instance.onError = (error, stack) {
debugPrint('Uncaught platform error: $error\n$stack');
return true;
};
try {
await PrefUtil.init(); await PrefUtil.init();
} catch (e, stack) {
debugPrint('PrefUtil.init failed, continuing with defaults: $e\n$stack');
}
runApp(const MainApp()); runApp(const MainApp());
},
(error, stack) {
debugPrint('Uncaught zone error: $error\n$stack');
},
);
} }
final class MainApp extends StatelessWidget { final class MainApp extends StatelessWidget {
+9 -1
View File
@@ -69,7 +69,11 @@ class _SettingsState extends State<Settings> {
Future<void> _save() async { Future<void> _save() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
final urlOk = await PrefUtil.setValue('base_url', _baseUrlController.text); try {
final urlOk = await PrefUtil.setValue(
'base_url',
_baseUrlController.text,
);
if (!mounted) return; if (!mounted) return;
final keyOk = await PrefUtil.setValue( final keyOk = await PrefUtil.setValue(
'api_key', 'api_key',
@@ -84,6 +88,10 @@ class _SettingsState extends State<Settings> {
} else { } else {
UISnackbars.showError(context, 'Failed to save settings'); UISnackbars.showError(context, 'Failed to save settings');
} }
} catch (e) {
if (!mounted) return;
UISnackbars.showError(context, 'Failed to save settings: $e');
}
} }
@override @override
+4
View File
@@ -59,6 +59,10 @@ LogInterceptor _buildLogInterceptor() => LogInterceptor(
); );
String dioErrorToUserMessage(Object error) { String dioErrorToUserMessage(Object error) {
if (error is TypeError || error is FormatException) {
debugPrint('Backend response shape error: $error');
return 'Unexpected response shape from backend.';
}
if (error is! DioException) { if (error is! DioException) {
debugPrint('Non-Dio error from backend call: $error'); debugPrint('Non-Dio error from backend call: $error');
return 'Unexpected error.'; return 'Unexpected error.';