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
+29 -4
View File
@@ -1,3 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:frontend/theme/catppuccin_mocha_theme.dart';
import 'package:frontend/utils/pref_utils.dart';
@@ -5,10 +8,32 @@ import 'package:provider/provider.dart';
import 'navigation.dart';
import 'theme/gruvbox_theme.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PrefUtil.init();
runApp(const MainApp());
void main() {
runZonedGuarded(
() async {
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();
} catch (e, stack) {
debugPrint('PrefUtil.init failed, continuing with defaults: $e\n$stack');
}
runApp(const MainApp());
},
(error, stack) {
debugPrint('Uncaught zone error: $error\n$stack');
},
);
}
final class MainApp extends StatelessWidget {
+22 -14
View File
@@ -69,20 +69,28 @@ class _SettingsState extends State<Settings> {
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
final urlOk = await PrefUtil.setValue('base_url', _baseUrlController.text);
if (!mounted) return;
final keyOk = await PrefUtil.setValue(
'api_key',
XOR().xorEncode(_apiKeyController.text),
);
if (!mounted) return;
final themeOk = await context.read<AppState>().setTheme(_selectedTheme);
if (!mounted) return;
if (urlOk && keyOk && themeOk) {
BackendAPI.instance.reconfigureFromPrefs();
UISnackbars.showSuccess(context, 'Settings saved successfully');
} else {
UISnackbars.showError(context, 'Failed to save settings');
try {
final urlOk = await PrefUtil.setValue(
'base_url',
_baseUrlController.text,
);
if (!mounted) return;
final keyOk = await PrefUtil.setValue(
'api_key',
XOR().xorEncode(_apiKeyController.text),
);
if (!mounted) return;
final themeOk = await context.read<AppState>().setTheme(_selectedTheme);
if (!mounted) return;
if (urlOk && keyOk && themeOk) {
BackendAPI.instance.reconfigureFromPrefs();
UISnackbars.showSuccess(context, 'Settings saved successfully');
} else {
UISnackbars.showError(context, 'Failed to save settings');
}
} catch (e) {
if (!mounted) return;
UISnackbars.showError(context, 'Failed to save settings: $e');
}
}
+4
View File
@@ -59,6 +59,10 @@ LogInterceptor _buildLogInterceptor() => LogInterceptor(
);
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) {
debugPrint('Non-Dio error from backend call: $error');
return 'Unexpected error.';