Skip to content

[go_router] pop() not working when both top-level 'onEnter' and 'redirect' are async and the route have onExit callback #180002

Description

@enzod-lgtm

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
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
  characters:
    dependency: transitive
    description:
      name: characters
      sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
      url: "https://pub.dev"
    source: hosted
    version: "1.4.0"
  collection:
    dependency: transitive
    description:
      name: collection
      sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
      url: "https://pub.dev"
    source: hosted
    version: "1.19.1"
  flutter:
    dependency: "direct main"
    description: flutter
    source: sdk
    version: "0.0.0"
  flutter_web_plugins:
    dependency: transitive
    description: flutter
    source: sdk
    version: "0.0.0"
  go_router:
    dependency: "direct main"
    description:
      name: go_router
      sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a
      url: "https://pub.dev"
    source: hosted
    version: "17.0.1"
  logging:
    dependency: transitive
    description:
      name: logging
      sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
      url: "https://pub.dev"
    source: hosted
    version: "1.3.0"
  material_color_utilities:
    dependency: transitive
    description:
      name: material_color_utilities
      sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
      url: "https://pub.dev"
    source: hosted
    version: "0.11.1"
  meta:
    dependency: transitive
    description:
      name: meta
      sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
      url: "https://pub.dev"
    source: hosted
    version: "1.17.0"
  sky_engine:
    dependency: transitive
    description: flutter
    source: sdk
    version: "0.0.0"
  vector_math:
    dependency: transitive
    description:
      name: vector_math
      sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
      url: "https://pub.dev"
    source: hosted
    version: "2.2.0"
sdks:
  dart: ">=3.9.0 <4.0.0"
  flutter: ">=3.35.0"

Steps to reproduce

Using the provided code:

  1. tap "Go to Details" button ( context.push() )
  2. tap "Back" button ( context.pop() )
  3. page is not popped

Things to note:

  • both declared routes are at the same level
  • navigation is done via context.push() and context.pop(), not context.go()

Problem:

This problem appears only when both top-level onEnter and top-level redirect callbacks return a Future AND the route you try to pop has an onExit callback (async or sync).

In my example, the bug disappears if you either:

  • remove the onEnter
  • make onEnter synchronous
  • remove the redirect
  • make redirect synchronous
  • remove the onExit callback from the route being pop

Also keep in mind that go_router_builder systematically creates an onExit callback (that returns true) for the generated routes, even when no override is provided, which is different from the behavior of a "manual" GoRoute, because not giving an onExit callback to GoRoute the constructor will lead to onExit being null.

So this problem can pop when migrating to go_router_builder, because it will add a default onExit callbacks to GoRoute that previously had a null onExit.

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
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(_) {
    return MaterialApp.router(
      routerConfig: GoRouter(
        debugLogDiagnostics: true,
        onEnter: (_, __, ___, ____) => Future.value(const Allow()), // <=======
        redirect: (_, __) => Future.value(null), // <=======
        routes: <RouteBase>[
          GoRoute(
            path: '/',
            builder: (_, __) => const HomePage(),
          ),
          GoRoute(
            path: '/details',
            builder: (_, __) => const DetailsPage(),
            onExit: (_, __) => Future.value(true), // <=======
          ),
        ],
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Home page'),
            ElevatedButton(
              onPressed: () => context.push('/details'), // <=======
              child: const Text('Go to Details'),
            ),
          ],
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  const DetailsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Details page'),
            ElevatedButton(
              onPressed: () => context.pop(), // <=======
              child: const Text('Back'),
            ),
          ],
        ),
      ),
    );
  }
}

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:

[GoRouter] Full paths for routes:
           ├─/ (HomePage)
           └─/details (DetailsPage)
[GoRouter] setting initial location null
[GoRouter] Using MaterialApp configuration
[GoRouter] pushing /details

Pressing "Back" button on Details page (page is not popped):

[GoRouter] popping /
[GoRouter] restoring /

Pressing a second time the "Back" button:

[GoRouter] popping /
[GoRouter] restoring /
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: Bad state: Future already completed
#0      _AsyncCompleter.complete (dart:async/future_impl.dart:97:31)
#1      ImperativeRouteMatch.complete (package:go_router/src/match.dart:489:15)
#2      GoRouterDelegate._completeRouteMatch (package:go_router/src/delegate.dart:187:14)
#3      GoRouterDelegate._handlePopPageWithRouteMatch.<anonymous closure> (package:go_router/src/delegate.dart:167:9)
<asynchronous suspension>

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.38.4, on macOS 26.2 25C56 darwin-arm64, locale en-FR) [7.4s]
    • Flutter version 3.38.4 on channel stable at /Users/REDACTED/fvm/versions/3.38.4
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 66dd93f9a2 (2 weeks ago), 2025-12-03 14:56:10 -0800
    • Engine revision a5cb96369e
    • Dart version 3.10.3
    • DevTools version 2.51.1
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging

[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [9.7s]
    • Android SDK at /Users/REDACTED/Library/Android/sdk
    • Emulator version 36.2.12.0 (build_id 14214601) (CL:N/A)
    • Platform android-36, build-tools 36.1.0
    • Java binary at: /Library/Java/JavaVirtualMachines/openjdk-17.jdk/Contents/Home/bin/java
      This JDK is specified in your Flutter configuration.
      To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment Homebrew (build 17.0.13+0)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 26.2) [13.3s]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 17C52
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web [11ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Connected device (4 available) [8.6s]
    • iPhone 15 Pro (iOS 26.2) (mobile) • 8630355B-90A8-450F-A036-C213C21DC870 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-26-2 (simulator)
    • iPhone 15 Pro (mobile)            • 3D789230-DF92-4EF7-89E6-1E9072D77DEA • ios            • com.apple.CoreSimulator.SimRuntime.iOS-26-1 (simulator)
    • macOS (desktop)                   • macos                                • darwin-arm64   • macOS 26.2 25C56 darwin-arm64
    • Chrome (web)                      • chrome                               • web-javascript • Google Chrome 140.0.7339.133

[✓] Network resources [281ms]
    • All expected network resources are available.

• No issues found!

Activity

  1. darshankawar commented on Dec 18, 2025

    @darshankawar
    Member

    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
    
  2. added
    packageflutter/packages repository. See also p: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Dec 18, 2025
  3. added
    P2Important issues not at the top of the work list
    on Dec 31, 2025
  4. pedrisbel commented on Jan 14, 2026

    @pedrisbel

    Same issue but only in Android/IOS.

  5. schindler-s commented on Feb 3, 2026

    @schindler-s

    go_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 onEnter callbacks, but we do use a ShellRoute combined with async redirect callbacks (returning Future.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 onExit on 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 onExit lead to an invalid pop state, potentially completing the same route match future more than once (similar to the Bad state: Future already completed error).

    Even when async callbacks simply return Future.value(null) or Future.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 with Navigator.of(context).pop() instead of context.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')),
          ],
        );
      }
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listfound in release: 3.38Found to occur in 3.38found in release: 3.40Found to occur in 3.40has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.team-ecosystemOwned by Ecosystem teamtriaged-ecosystemTriaged by Ecosystem team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions