Chuyển đến nội dung chính

Flutter error handling: catching what actually reaches users

A crash-free rate of 99.8% sounds excellent until you realise it only counts crashes your reporting tool was wired to see. In Flutter, an error can escape through four different doors, and most apps only guard one or two of them.

The four doors

DoorCatchesMissed if unwired
FlutterError.onErrorErrors inside the framework: build, layout, paint, gesture callbacksRed screens, silent layout failures
PlatformDispatcher.instance.onErrorUncaught async errors in the root zoneMost Future failures
Isolate.current.addErrorListenerErrors in isolates you spawnedEvery background-compute failure
Native crash handlerPlatform-level crashes, plugin native codeAnything that kills the process

Here is all four, wired once at startup:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  final crashlytics = FirebaseCrashlytics.instance;

  // 1. Framework errors.
  FlutterError.onError = (details) {
    crashlytics.recordFlutterFatalError(details);
    if (kDebugMode) FlutterError.presentError(details);
  };

  // 2. Uncaught async errors reaching the platform.
  PlatformDispatcher.instance.onError = (error, stack) {
    crashlytics.recordError(error, stack, fatal: true);
    return true; // handled
  };

  // 3. Errors from isolates spawned by this one.
  Isolate.current.addErrorListener(RawReceivePort((List<dynamic> pair) {
    crashlytics.recordError(pair.first, StackTrace.fromString(pair.last));
  }).sendPort);

  runApp(const MyApp());
}

Three details worth pausing on.

Returning true from PlatformDispatcher.onError tells the engine the error is handled and should not be re-thrown. Returning false lets it propagate to the default handler as well, which usually means a duplicate report.

presentError only in debug. In release you do not want the framework’s own error output; you want the report. In debug you very much want the red screen, because that is how you notice.

The isolate listener only covers isolates you spawn, and only those spawned after it is registered. A compute() call inherits nothing automatically — errors inside it surface as a failed Future, which door two catches, provided nobody swallowed it with an empty catch.

runZonedGuarded used to be the standard advice and is now largely superseded by PlatformDispatcher.onError for this purpose. Using both is not harmful, but is usually redundant; pick one and know which.

The red screen your users see

In release builds, a build-method exception replaces the widget with a grey box. That is better than a crash and worse than a design decision:

ErrorWidget.builder = (FlutterErrorDetails details) {
  if (kDebugMode) return ErrorWidget(details.exception);

  return const Material(
    child: Center(
      child: Padding(
        padding: EdgeInsets.all(24),
        child: Text(
          'Something went wrong here. Try again in a moment.',
          textAlign: TextAlign.center,
        ),
      ),
    ),
  );
};

Set this once. It costs ten lines and turns an alarming grey rectangle into something that reads as intentional.

For a subtree that can fail independently — a feed item, a chart, a plugin-backed view — a small wrapper prevents one failure from blanking the screen:

class ErrorBoundary extends StatefulWidget {
  const ErrorBoundary({super.key, required this.child, required this.fallback});

  final Widget child;
  final Widget fallback;

  @override
  State<ErrorBoundary> createState() => _ErrorBoundaryState();
}

Flutter has no built-in error boundary that catches child build errors the way React does — FlutterError.onError fires globally, so the practical version scopes by rebuilding a fallback when a known failure mode is detected, rather than by catching arbitrary child exceptions.

Reports you can act on

A stack trace alone rarely tells you enough. The difference between a bug you can fix and one you stare at is context attached before the crash:

final class CrashContext {
  static Future<void> setUser(String? id) =>
      FirebaseCrashlytics.instance.setUserIdentifier(id ?? 'anonymous');

  static Future<void> setScreen(String route) =>
      FirebaseCrashlytics.instance.setCustomKey('current_route', route);

  static void breadcrumb(String message) =>
      FirebaseCrashlytics.instance.log(message);
}

Log the route on every navigation, the id of whatever record the screen is showing, and the last network call attempted. Then a report reads “crashed on /orders/882 after GET /orders/882 returned 500” rather than “null check operator on a null value”.

Do not log personal data. A crash report is a copy of your users’ information in a third-party system; user ids and record ids are usually appropriate, email addresses and message contents are not.

Symbolication, or the reports are useless

Release builds compiled with --obfuscate --split-debug-info=<dir> produce stack traces of meaningless symbols. The mapping lives in the directory you specified, and it is different for every build:

flutter build appbundle --obfuscate --split-debug-info=build/symbols/$VERSION

Two rules that people learn the hard way:

  1. Archive the symbols directory per version, in CI, alongside the artifact. Without the exact file for the exact build, a report cannot be decoded — and rebuilding from the same source does not reproduce it.
  2. Upload symbols as part of the release job, not manually. A manual step is a step that gets skipped on the release where it matters.

flutter symbolize -i trace.txt -d build/symbols/1.4.2/app.android-arm64.symbols decodes a trace by hand when needed.

What to catch and what to let crash

The instinct to wrap everything in try/catch produces an app that fails silently and behaves strangely. A rule that holds up:

  • Catch what you can act on. A network timeout has a retry. A parse failure has a fallback. Catch those, handle them, report at a non-fatal level.
  • Let programming errors crash in debug. A null assertion failing means your model of the code is wrong. Catching it hides that.
  • In release, degrade rather than die — but always report. A catch block with no report is the mechanism by which a bug survives for months.
try {
  return await _api.fetchOrders();
} on TimeoutException catch (e, s) {
  FirebaseCrashlytics.instance.recordError(e, s, fatal: false);
  return _cache.orders ?? const [];
}

Catching on TimeoutException rather than bare catch is the part that matters. A bare catch also swallows the NoSuchMethodError from your own typo.

FAQ

Why do I see errors in the console that never reach Crashlytics?

Almost always an unwired door — most often PlatformDispatcher.onError, or an error inside a catch that logs and continues.

Does this work with Sentry or another tool instead?

Yes. The four doors are Flutter’s, not Firebase’s; the SDKs differ only in the recording call.

Should FlutterError.onError report as fatal?

A framework error usually leaves the app running with broken UI. recordFlutterFatalError puts it in the crash-free metric, which is arguably right; recordFlutterError reports it as non-fatal. Choose one and be consistent, or your metrics will not mean anything.

How do I test that reporting works?

Add a hidden debug action that throws in each of the four contexts, run a release build, and confirm four reports arrive. Do this once per release cycle — reporting silently breaks after dependency upgrades.

What about errors during main() before reporting is initialised?

Initialise reporting as early as possible, and keep the work before it minimal. Anything failing before that point is invisible by construction.


The four error-handling entry points, ErrorWidget.builder, obfuscation and symbolication commands described here are documented in the Flutter references linked above. The catch-what-you-can-act-on rule, the breadcrumb practice, the symbol-archiving advice and the release-cycle verification ritual are my own judgement from operating Flutter apps in production. Crash-reporting SDK APIs change — verify the recording calls against the version you depend on.


Originally published on FlutterCook. Read the latest version there — that copy is the one kept up to date.

Nhận xét

Bài đăng phổ biến từ blog này

5 concepts every Flutter dev should know

  Phụ lục: State management architecture Testing IDE Shortcuts Platform channel Maintaining a project Tôi đã làm việc với Flagship trong một thời gian dài, và đây là những điều mà tôi phát hiện ra là điều cần phải có đối với bất kỳ nhà phát triển Flagship nào, về tổng thể nó sẽ khiến bạn trở thành một nhà phát triển Flagship giỏi trong thời gian dài. 1. State management architecture Đây là một trong những chủ đề quan trọng nhất trong cộng đồng thiết bị rung, nó khá quan trọng nếu bạn muốn duy trì một dự án rung kích thước trung bình hoặc lớn. Nó sẽ giúp tạo một dự án suôn sẻ và thêm các tính năng mới một cách hoàn hảo.  2. Testing Đây là một chủ đề duy nhất mà tôi không hiểu tại sao nó lại quan trọng trước đó trong sự nghiệp của tôi, nhưng khi tôi tiến lên trong sự nghiệp của mình và có kinh nghiệm với nhiều dự án và vấn đề xảy ra trong môi trường sản xuất. Tôi đã nhận ra một cách khó khăn, tại sao điều này lại quan trọng như vậy. Nếu bạn vẫn muốn có thêm lý do để cân nhắc thử...

Thiết kế giao diện với DotNetBar (Phần 1)

Đây là phiên bản DotNetBar hỗ trợ C# và Visual Basic https://www.dropbox.com/s/wx80jpvgnlrmtux/DotNetBar.rar  , phiên bản này hỗ trợ giao diện Metro cực kỳ “dễ thương” Các bạn load về và cài đặt, khi cài đặt xong sẽ có source code mẫu của tất cả các control. Để sử dụng được các control của DotNetBar các bạn nhớ add item vào controls box. Thiết kế giao diện với DotNetBar, giao diện sẽ rất đẹp. Link các video hướng dẫn chi tiết cách sử dụng và coding: http://www.devcomponents.com/dotnetbar/movies.aspx Hiện tại DotNetBar có rất nhiều công cụ cực mạnh, trong đó có 3 công cụ dưới đây: DotNetBar for Windows Forms Requires with Visual Studio 2003, 2005, 2008, 2010 or 2012.   DotNetBar for WPF Requires with Visual Studio 2010 or 2012 and Windows Presentation Foundation.   DotNetBar for Silverlight Requires with Visual Studio 2010 or 2012 and Silverlight. Dưới đây là một số hình ảnh về các control trong DotnetBar.   Metro User Interface  controls with Metro Tiles, toolba...

Announcing Flutter 2

  Phụ lục: Flutter on the web Flutter 2 on desktops, foldables, and embedded devices The growing Flutter ecosystem Dart: The secret sauce behind Flutter Flutter 2: Available now Hôm nay, chúng tôi sẽ công bố Flutter 2: một bản nâng cấp lớn cho Flutter cho phép các nhà phát triển tạo các ứng dụng đẹp, nhanh chóng và di động cho bất kỳ nền tảng nào. Với Flutter 2, bạn có thể sử dụng cùng một cơ sở mã để gửi các ứng dụng gốc cho năm hệ điều hành: IOS, Android, Windows, macOS và Linux; cũng như trải nghiệm web nhắm mục tiêu các trình duyệt như Chrome, Firefox, Safari hoặc Edge. Flutter thậm chí có thể được nhúng vào ô tô, TV và thiết bị gia dụng thông minh, mang đến trải nghiệm di động và lan tỏa nhất cho thế giới điện toán xung quanh. Mục tiêu của chúng tôi là thay đổi cơ bản cách các nhà phát triển nghĩ về việc xây dựng ứng dụng, bắt đầu không phải với nền tảng bạn đang nhắm mục tiêu mà là với trải nghiệm bạn muốn tạo. Flutter cho phép bạn tạo ra những trải nghiệm tuyệt đẹp trong đó ...