skip to content

Error Hooks & Reporting

FlutterError.onError sees framework errors, PlatformDispatcher.instance.onError takes uncaught async ones, and ErrorWidget.builder swaps the red screen. Interviewers ask how crashes reach a reporter.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Flutter, which errors reach FlutterError.onError and which reach PlatformDispatcher.instance.onError, and why does an app need both?

level: middleimportance: must knowfreq 58%

answer

  1. two hooks, two routes
  2. caught inside framework callbacks
  3. no Flutter callback on the stack
  4. ErrorCallback returns bool
  5. assign both in main before runApp

basics

~20 s

FlutterError.onError receives errors the framework catches inside its own callbacks: build, layout, paint, gesture handlers. PlatformDispatcher.instance.onError receives uncaught errors with no Flutter callback on the stack, such as a failed await in an async onPressed. Reporting needs both.

solid answer

~40 s

Flutter wraps its own callbacks (`build`, layout, paint, gesture handlers, frame callbacks) in try/catch and routes what it catches to `FlutterError.reportError`, which calls the static `FlutterError.onError`. Its default is `FlutterError.presentError`, which prints to the console. An error that escapes outside such a callback goes to `PlatformDispatcher.instance.onError` instead: an error after an `await` in an `async` `onPressed`, a failed `MethodChannel.invokeMethod`, a throwing `Timer` callback. That hook is an `ErrorCallback` that returns `true` when it has handled the error and `false` to let the embedder's fallback print it. A crash-reporting setup assigns both in `main()` before `runApp`, because either one alone misses a whole class of errors.

code

dart · 24 lines
dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class CrashReporter {
  void report(Object error, StackTrace? stack, {required String source}) {
    debugPrint('[$source] $error');
  }
}

void main() {
  final reporter = CrashReporter();

  FlutterError.onError = (FlutterErrorDetails details) {
    FlutterError.presentError(details);
    reporter.report(details.exception, details.stack, source: details.library ?? 'flutter');
  };

  PlatformDispatcher.instance.onError = (Object error, StackTrace stack) {
    reporter.report(error, stack, source: 'uncaught');
    return true;
  };

  runApp(const MaterialApp(home: Scaffold(body: Center(child: Text('ok')))));
}

go deeper

for a junior

Remember the two names and the split: errors Flutter catches go to FlutterError.onError, uncaught ones go to PlatformDispatcher.instance.onError.

for a middle

Explain why an async onPressed escapes the gesture recognizer's try/catch, and what returning true or false from the dispatcher hook tells the engine.

for a senior

Show you can audit a reporting setup: both hooks set before runApp, console output kept, spawned isolates forwarded, and each target platform checked.

for a principal

Weigh which failures should count as crashes versus logged noise, and how the team keeps one owner for these global static hooks.

## Two routes for two kinds of failure A Flutter app fails in two structurally different ways, and each has its own hook: - **Errors the framework catches.** Flutter calls your code from inside its own machinery: a widget's `build`, a `RenderObject`'s layout and paint, a gesture recognizer invoking `onTap`, a scheduler frame callback. Each of those call sites wraps your code in `try`/`catch`, builds a `FlutterErrorDetails`, and passes it to `FlutterError.reportError`, which calls **`FlutterError.onError`**. - **Errors nobody catches.** Code that runs with no Flutter callback on the call stack cannot be caught by the framework. Examples are a `Future` that fails after an `await`, a `Timer` callback that throws, or a stream error with no listener for it. Such an error is unhandled in the root isolate, and the engine hands it to **`PlatformDispatcher.instance.onError`**. How an unhandled asynchronous error travels through Dart's zones before it reaches the engine is a Dart language topic. What matters here is the Flutter end: which static hook sees it. ## Route 1: FlutterError.onError `FlutterError.onError` is a static field of type `FlutterExceptionHandler?`, a function taking one `FlutterErrorDetails`. Its default value is `FlutterError.presentError`, which in turn defaults to `FlutterError.dumpErrorToConsole`. So out of the box, a framework-caught error is printed and the app keeps running. The `library` string on the details tells you which layer caught it: | Where it was thrown | `library` value in the details | |---|---| | a widget's `build`, a `LayoutBuilder` builder, a list item builder | `widgets library` | | `performLayout` or `paint` of a render object | `rendering library` | | a gesture callback such as `onTap` | `gesture` | | a scheduler frame or task callback | `scheduler library` | Some of these sites do more than report. A failed build is also replaced on screen by the widget returned from `ErrorWidget.builder`. ## Route 2: PlatformDispatcher.instance.onError `PlatformDispatcher` lives in `dart:ui` and is re-exported by `package:flutter/foundation.dart`. Its `onError` is an `ErrorCallback`: `bool Function(Object exception, StackTrace stackTrace)`. - Return **`true`** to say the error is handled. - Return **`false`** (or leave the hook `null`) and the platform embedding's fallback runs, typically printing to stderr. - The setter records the current zone, and the callback later runs in that zone. - It is **not** called for errors inside isolates you spawn. Those must be listened for on that isolate and forwarded to the root isolate. - The engine documents that the VM or process may still exit after the callback. Errors that terminate the process first never reach it. ## Why async onPressed is the classic trap A tap on a button reaches your `onPressed` through `GestureRecognizer.invokeCallback`, which catches synchronous throws and reports them with `library: 'gesture'`. Mark the callback `async` and that protection is gone. An `async` function never throws synchronously; any error, even one before the first `await`, completes the returned `Future` with an error. The button ignores that `Future`, the error goes unhandled, and it arrives at `PlatformDispatcher.instance.onError`, not `FlutterError.onError`. ## Wiring both 1. Create or initialise your crash reporter at the top of `main()`. 2. Assign `FlutterError.onError`: call `FlutterError.presentError(details)` to keep console output, then forward `details.exception` and `details.stack`. 3. Assign `PlatformDispatcher.instance.onError`: forward the error and stack, then return `true`. 4. Call `runApp` last, so both hooks are in place before the first frame builds. Crash-reporting SDKs usually do these assignments for you inside their init call. Knowing the two routes is how you check that the SDK, or your own code, covers both. ## Checking the wiring in a debug session Three deliberate failures show whether each route reaches your reporter: - **A throw inside a widget's `build`.** Expect it in the `FlutterError.onError` path, tagged `widgets library`, with the red `ErrorWidget` on screen. - **A throw inside a plain, non-`async` `onTap`.** Expect the same hook, tagged `gesture`, and no change on screen. - **A throw after an `await` in an `async` `onPressed`, or inside a `Timer` callback.** Expect it in the `PlatformDispatcher.instance.onError` path and nowhere else. If any of the three never arrives, the missing hook is the one that route uses. Remove the test throws before you commit. ## Common mistakes - Assuming `FlutterError.onError` sees every error, and then losing every failed network call made from an `async` handler. - Hooking only `PlatformDispatcher.instance.onError` and never seeing build, layout or paint failures, because the framework catches those first. - Returning `false` from the dispatcher hook after reporting, which also triggers the embedder's default printing. - Setting `FlutterError.onError = null`. The source documents that this silently ignores framework errors; it does not make them crash the app.

  • Does PlatformDispatcher.instance.onError see an exception thrown inside an isolate you spawned?
    No. The engine documents that the callback is not directly invoked by errors in child isolates of the root isolate. The code that spawns an isolate must listen for that isolate's errors and forward them to the root isolate, where they can be passed to the reporter explicitly.
  • Should you rely on PlatformDispatcher.instance.onError when the same app also ships to Flutter web?
    Not without testing it there. In the pinned 3.47 web engine the `onError` setter stores the callback and its zone, but no code path invokes it, so uncaught async errors on web do not arrive through this hook. Verify each target platform's reporting path instead of assuming mobile behaviour carries over.
  • Why must the dispatcher hook return a bool at all?
    The return value tells the engine whether the error was handled. `true` stops there; `false` hands the error to the platform embedding's fallback, which typically prints it to stderr. A reporter that has recorded the error returns `true` to avoid double logging.

saying these in an interview costs you the question

  • FlutterError.onError catches every error in the app, including async ones
  • An async onPressed is protected because onPressed is a Flutter callback
  • The bool returned by PlatformDispatcher.instance.onError is ignored
  • Errors in spawned isolates reach PlatformDispatcher.instance.onError automatically
  • Setting FlutterError.onError to null makes framework errors crash the app
open as a page

In Flutter, what is the red error screen, and what does a release-build user see when the same widget's build method throws?

level: juniorimportance: should knowfreq 52%

basics

~20 s

The red screen is an ErrorWidget built in place of a widget whose build threw. Debug builds show the exception in yellow on red; profile and release builds, with asserts off, show a plain grey box with no text.

open as a page

When a Flutter app replaces FlutterError.onError, why should the handler still call FlutterError.presentError, and what does FlutterErrorDetails carry?

level: middleimportance: should knowfreq 30%

basics

~20 s

FlutterError.onError defaults to presentError, which prints the error and is the hook the IDE's structured error view replaces. A custom handler that skips presentError loses that output. FlutterErrorDetails carries the exception, stack, library, context, an information collector and a silent flag.

open as a page

Users of a Flutter release build see a grey box where a crashed widget should be, yet the crash reporter shows nothing; what is missing, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The grey box means the framework caught a build exception and sent it to FlutterError.onError, whose default only prints. Reporting wired only to uncaught errors never sees it; forward FlutterError.onError to the reporter, then fix the build.

open as a page