Skip to content

Production Concerns

In production you want to hear about crashes before your users complain. Flutter errors arrive through two channels, and you need both:

void main() {
// 1. Errors thrown inside the Flutter framework (build, layout, paint).
FlutterError.onError = (details) {
FlutterError.presentError(details);
reportToCrashService(details.exception, details.stack);
};
// 2. Errors outside the framework (async gaps, callbacks) — a guarded zone.
runZonedGuarded(() {
runApp(const MyApp());
}, (error, stack) {
reportToCrashService(error, stack);
});
}

FlutterError.onError catches errors the framework raises; runZonedGuarded catches everything else (uncaught async errors, isolate errors via PlatformDispatcher.instance.onError). You can also replace the red ErrorWidget with a friendly fallback so a single widget failure doesn’t show users a crash screen.

Wire those handlers to a service — Sentry, Firebase Crashlytics — so real-device crashes come back with a stack trace, device info, and breadcrumbs. Two rules that matter:

  • Upload symbols/debug info for release builds (AOT strips symbols; without the mapping, stack traces are unreadable obfuscated addresses).
  • Log deliberately. print is stripped/ignored in release; use a real logger and scrub anything sensitive before it leaves the device.

Smoothness is a budget: ~16ms per frame for 60fps, ~8ms for 120fps. Miss it and the frame “janks.” Diagnose in profile mode (never debug):

flowchart LR
  build["build
(your widgets)"] --> layout["layout"] --> paint["paint"] --> raster["raster
(GPU)"]
  raster --> budget["must fit ~16ms
or the frame janks"]
Where a frame's time goes

DevTools’ timeline shows per-frame cost split into UI (Dart: build/layout/paint) and raster (GPU). Common culprits: rebuilding too much (missing const, state too high in the tree), expensive work in build, big un-cached images, and missing RepaintBoundary around constantly-animating subtrees.

Automate the pipeline so quality isn’t a matter of memory: on every push, run flutter analyze, flutter test, then flutter build for each platform, and ship to TestFlight / Play internal tracks. Tools like Codemagic, Fastlane, and GitHub Actions handle the Flutter-specific signing and store upload.

A production checklist:

  • Global error handling wired (FlutterError.onError + runZonedGuarded) to a crash service, with symbols uploaded.
  • A friendly ErrorWidget fallback in release.
  • Profiled in profile mode; no obvious jank on the target devices.
  • Flavors for dev/staging/prod; no secrets in source (--dart-define / secure storage).
  • CI running analyze + tests + builds on every change.
Why do you need both FlutterError.onError and runZonedGuarded?
Why must you upload symbols/debug info for release crash reports?
In which mode should you profile performance?
Which is a common cause of jank?