Direct answer
A custom ZoneSpecification with handleUncaughtError reliably intercepts errors from unawaited Future objects in both JIT and AOT modes. The Dart runtime routes unhandled asynchronous errors through the current zone's error handler regardless of whether the Future was awaited. This behavior holds under --obfuscate compilation.
How the zone captures unawaited Future errors
When a Future completes with an error and no catchError or await is attached, the Dart VM schedules an "unhandled async error" event. That event is delivered to Zone.current.handleUncaughtError of the zone where the Future was created (or the zone active when the error propagates). runZonedGuarded or a manual ZoneSpecification installed early in main() becomes the root handler for all such events.
Key requirement: the zone must be installed before any asynchronous work starts. Late installation misses errors from Futures already in flight.
Stack traces under --obfuscate
AOT compilation with --obfuscate renames functions and classes to short identifiers (e.g., a, b). The raw stack trace captured inside handleUncaughtError reflects these minified names. To recover original source locations you need the symbol map emitted at compile time:
dart compile aot --obfuscate --symbol-map=symbols.json bin/main.dart
The symbols.json file maps each obfuscated identifier to its original qualified name, file, and line number. Post-process captured traces by feeding them through the symbol map. The SDK provides dart --symbol-map=symbols.json for interactive de-obfuscation; for automated pipelines you can parse the JSON yourself or use a tool like package:stack_trace with a custom Frame mapper.
Practical verification steps
- Write a minimal program that spawns an unawaited
Future throwing a distinctive error.
- Compile with
dart compile aot --obfuscate --symbol-map=symbols.json.
- Run the AOT snapshot and confirm the zone handler logs the error with an obfuscated stack trace.
- Apply the symbol map to the logged trace and verify original function names and line numbers appear.
Assumptions and open questions
- Assumes Dart SDK ≥ 2.12 (when
--obfuscate and --symbol-map stabilized). Symbol map format may evolve across minor versions.
- Private members are aggressively renamed; symbol maps may omit local variable names, limiting trace readability for inlined code.
- Flutter engine embedders or custom AOT runners can alter isolate/zone semantics—verify on your target platform if not using the standard Dart VM.
- The
logging package integrates with zone handlers but does not de-obfuscate automatically; you must post-process.
One diagnostic detail that changes the recommendation
Are you running multiple isolates? Each isolate has its own zone hierarchy. If yes, you must install the error-handling zone in every isolate entry point, not just the root isolate.