Skip to content

[go_router] Fix assertion failure when onException fires on initial navigation#12216

Draft
davidmigloz wants to merge 2 commits into
flutter:mainfrom
davidmigloz:upstream-onexception-initial-nav-fix
Draft

[go_router] Fix assertion failure when onException fires on initial navigation#12216
davidmigloz wants to merge 2 commits into
flutter:mainfrom
davidmigloz:upstream-onexception-initial-nav-fix

Conversation

@davidmigloz

@davidmigloz davidmigloz commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

When onException is provided and fires during the initial navigation (e.g. a deep-linked route blocked by onEnter returning Block.stop()), parserExceptionHandler in router.dart returns routerDelegate.currentConfiguration. On the initial navigation nothing has committed yet, so that configuration is empty, and GoRouterDelegate.setNewRoutePath crashes on assert(configuration.isNotEmpty || configuration.isError).

Crash sequence:

  1. App starts with initialLocation pointing at a route.
  2. onEnter returns Block.stop() for it.
  3. The parser builds an error RouteMatchList (isError == true) and invokes onException.
  4. parserExceptionHandler synchronously returns routerDelegate.currentConfiguration — still empty.
  5. setNewRoutePath asserts configuration.isNotEmpty || configuration.isError — neither holds — crash.

The fix has two parts:

  1. router.dart: fall back to the error match list when the current configuration is invalid — neither non-empty nor isError — satisfying the assertion's isError branch.
  2. parser.dart: in the onCanNotEnter no-prior-route branch, defer the onParserException call to a microtask (the same pattern as the existing deferral for the onEnter then callback nearby). Without this, a recovery navigation made synchronously inside onException — e.g. router.go('/fallback'), as in the issue's repro — is silently discarded by the Router's intent-token churn, leaving the app parked on the error state. Deferring past the in-flight parse lets synchronous recovery work the way onException normally does.

Behavior is unchanged everywhere else: non-initial navigations return the non-empty currentConfiguration as before, the plain unmatched-initial-route path keeps its fully synchronous onException call (existing redirect tests depend on it, and that path never drops the re-entrant navigation), and without onException neither code path is reached.

Tests in exception_handling_test.dart:

  • One test calls router.go('/fallback') synchronously from inside onException and asserts full recovery (fallback rendered, protected absent, router.state.uri.path == '/fallback'); a second drives the app-side deferred-via-scheduleMicrotask pattern. Verified red/green for each half: without the router.dart fallback the delegate assertion fires at delegate.dart:221; without the parser.dart deferral the synchronous recovery is silently dropped and the fallback never renders.
  • The router.dart fallback is also covered independently of the deferred path. An unmatched initial navigation with a non-navigating onException now renders the default error screen; before the fix the app just stayed blank, because the empty current configuration hit setNewRoutePath's equality early-return. And a second unmatched navigation no longer replaces the first installed error configuration: the fallback preserves any valid current configuration, whether non-empty or isError.

Fixes flutter/flutter#189582

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

…avigation

When onException fires during the initial navigation (e.g., an initial
deep link blocked by onEnter returning Block.stop()),
routerDelegate.currentConfiguration is empty. Returning it directly from
parserExceptionHandler causes GoRouterDelegate.setNewRoutePath to hit
`assert(configuration.isNotEmpty || configuration.isError)`.

Fall back to the error match list (isError=true) when the current
configuration is empty, satisfying the assertion while any recovery
navigation queued from onException is processed.

Additionally, defer the onParserException call itself to a microtask on
that same initial-navigation, blocked-with-no-prior-route path. Calling
it synchronously let the app crash-prevention fix land, but a
router.go() made synchronously from onException was silently discarded:
Flutter's Router mints a new intent token for the re-entrant parse while
the initial parse is still in-flight, dropping the outer result. The
deferral is scoped to that one parser.dart call site so ordinary initial
navigation exceptions (e.g. a plain unmatched route) keep calling
onException synchronously, as existing tests rely on.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

p: go_router triage-framework Should be looked at in framework triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[go_router] Assertion failure in setNewRoutePath when onException fires on the initial navigation

1 participant