Repository navigation
[go_router] pop() not working when both top-level 'onEnter' and 'redirect' are async and the route have onExit callback #180002
Description
Activity
- addedin triagePresently being triaged by the triage teamPresently being triaged by the triage team
on Dec 18, 2025 Thanks for the detailed report. Seeing the same behavior as reported with latest sdk versions.
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: Bad state: Future already completed
This could be similar / related to #160696
stable: 3.38.4 master: 3.40.0-1.0.pre-192- addedp: go_routerThe go_router packageThe go_router packagepackageflutter/packages repository. See also p: labels.flutter/packages repository. See also p: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onfound in release: 3.38Found to occur in 3.38Found to occur in 3.38found in release: 3.40Found to occur in 3.40Found to occur in 3.40team-frameworkOwned by Framework teamOwned by Framework teamand removedin triagePresently being triaged by the triage teamPresently being triaged by the triage team
on Dec 18, 2025 - addedP2Important issues not at the top of the work listImportant issues not at the top of the work listtriaged-frameworkTriaged by Framework teamTriaged by Framework team
on Dec 31, 2025 Same issue but only in Android/IOS.
Reacted by adonisRodxandergo_router async pop issue – related reproduction
We are seeing the same underlying problem in a slightly different (but likely related) setup, starting with go_router 16.0.0.
In our case, we do not use
onEntercallbacks, but we do use aShellRoutecombined with asyncredirectcallbacks (returningFuture.value(null)). From 15.1.3 → 16.0.0,context.pop()stopped working reliably.Observations
The problem appears whenever multiple async lifecycle callbacks participate in the route resolution / pop chain, even if they are effectively no-ops.
Affected async callbacks in our setup:
- async top-level
redirect - async
ShellRoute.redirect - async
onExiton the route being popped
Observed behavior:
- Removing async redirects (or making them synchronous) restores correct
context.pop()behavior - Keeping redirects async, but reducing their number, changes whether the pop works or not
- The issue reproduces on Web and Android, on both stable (3.38.9) and beta (3.41.0-0.2.pre)
This aligns well with the hypothesis in the original issue that async callbacks combined with
onExitlead to an invalid pop state, potentially completing the same route match future more than once (similar to theBad state: Future already completederror).Even when async callbacks simply return
Future.value(null)orFuture.value(true), they are still sufficient to trigger the issue. This suggests the problem is not the async work itself, but the presence of async boundaries in the routing lifecycle.Additional observation
In this ShellRoute-based example, the issue appears to be specific to go_router's pop handling:
context.pop()does not pop the route- Replacing it with
Navigator.of(context).pop()works as expected and correctly pops the route
Edit: If onExit itself awaits additional async work (such as showing a confirmation dialog), Navigator.of(context).pop() also stops working.
Edit2: Popping the dialog withNavigator.of(context).pop()instead ofcontext.pop()leads to expected behaviour again.
This further suggests that the problem is related to go_router's internal imperative pop lifecycle rather than Flutter's Navigator itself.
ShellRoute-based minimal reproduction
Code Example
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(const MinimalRouterApp()); } class MinimalRouterApp extends StatelessWidget { const MinimalRouterApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'Router Minimal App', routerConfig: GoRouter( debugLogDiagnostics: true, routerNeglect: true, redirectLimit: 10, redirect: (context, state) => Future.value(null), routes: [ GoRoute( path: "/", pageBuilder: (context, state) => NoTransitionPage(child: Home()), ), ShellRoute( redirect: (context, state) => Future.value(null), pageBuilder: (context, state, child) => NoTransitionPage(child: child), routes: [ GoRoute( path: "/main", pageBuilder: (context, state) => NoTransitionPage(child: ShellMain()), routes: [ GoRoute( path: "sub", pageBuilder: (context, state) => NoTransitionPage(child: ShellSub()), onExit: (_, _) => Future.value(true), ), ], ), ], ), ], ), ); } } class Home extends StatelessWidget { const Home({super.key}); @override Widget build(BuildContext context) { return Column( spacing: 12, mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Home', style: Theme.of(context).textTheme.headlineSmall), FilledButton( onPressed: () => context.go("/main"), child: const Text('Enter shell'), ), ], ); } } class ShellMain extends StatelessWidget { const ShellMain({super.key}); @override Widget build(BuildContext context) { return Column( spacing: 12, mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Shell Main', style: Theme.of(context).textTheme.headlineSmall), FilledButton( onPressed: () => context.go("/main/sub"), child: const Text('Go to sub route'), ), ], ); } } class ShellSub extends StatelessWidget { const ShellSub({super.key}); @override Widget build(BuildContext context) { return Column( spacing: 12, mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Shell Sub', style: Theme.of(context).textTheme.headlineSmall), OutlinedButton(onPressed: context.pop, child: const Text('Pop')), ], ); } }
- async top-level
- added a commit that references this issue
on Apr 14, 2026 - added a commit that references this issue
on Jun 10, 2026 - added a commit that references this issue
on Jun 19, 2026 - addedteam-ecosystemOwned by Ecosystem teamOwned by Ecosystem teamtriaged-ecosystemTriaged by Ecosystem teamTriaged by Ecosystem teamand removedteam-frameworkOwned by Framework teamOwned by Framework teamtriaged-frameworkTriaged by Framework teamTriaged by Framework team
on Aug 26, 2026
What package does this bug report belong to?
go_router
What target platforms are you seeing this bug on?
Android, iOS
Have you already upgraded your packages?
Yes
Dependency versions
pubspec.lock
Steps to reproduce
Using the provided code:
Things to note:
context.push()andcontext.pop(), notcontext.go()Problem:
This problem appears only when both top-level
onEnterand top-levelredirectcallbacks return aFutureAND the route you try to pop has anonExitcallback (async or sync).In my example, the bug disappears if you either:
onEnteronEntersynchronousredirectredirectsynchronousonExitcallback from the route being popAlso keep in mind that
go_router_buildersystematically creates anonExitcallback (that returnstrue) for the generated routes, even when no override is provided, which is different from the behavior of a "manual"GoRoute, because not giving anonExitcallback toGoRoutethe constructor will lead toonExitbeingnull.So this problem can pop when migrating to
go_router_builder, because it will add a defaultonExitcallbacks to GoRoute that previously had a nullonExit.Expected results
When tapping the "Back" button on the Details page, it should pop the current route and show the Home page
Actual results
When tapping the "Back" button on the Details page, the route is not popped and nothing happens.
If I tap again, an Exception is thrown. (see the stacktrace below)
Code sample
Code sample
Screenshots or Videos
Screenshots / Video demonstration
Not working:
https://github.com/user-attachments/assets/5f0c78e9-4547-4526-b7b2-58ada099c6e8
Working (no onEnter OR no redirect):
https://github.com/user-attachments/assets/9a03915b-30a0-4ae3-aac6-f614765eaaba
Logs
Logs
Launching the app and tapping "Go To Details" button:
Pressing "Back" button on Details page (page is not popped):
Pressing a second time the "Back" button:
Flutter Doctor output
Doctor output