From ef714c4bd9dd6f43531694555601956233f21e30 Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Fri, 15 May 2026 02:01:18 +0530 Subject: [PATCH 1/2] feat: Improve Navigator pop type mismatch error messages Updated the error handling in Navigator.pop and maybePop to provide a detailed FlutterError when a result type doesn't match the Route's expected type. This replaces the generic and cryptic TypeError with a clear message including the expected type, actual type, and route details --- .../flutter/lib/src/widgets/navigator.dart | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 4aa5d7f8822af..66865bbd27913 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -641,6 +641,28 @@ abstract class Route extends _RoutePlaceholder { return _navigator?._firstRouteEntryWhereOrNull(_RouteEntry.isRoutePredicate(this))?.isPresent ?? false; } + + /// Asserts that the given result is of a type that can be consumed by this route. + /// + /// This is used by [Navigator] to provide a clear error message when route is popped with mismatched result type. + bool _debugCheckCanConsumeResult(dynamic result, {required String methodName}) { + if (result is! T?) { + throw FlutterError.fromParts([ + ErrorSummary( + 'A request was made to pop a route with a result of type ${result.runtimeType}, but the route expected a value of type $T.', + ), + ErrorDescription( + 'This usually happens when the type provided to Navigator.$methodName() ' + 'is not a subtype of the type expected by the Route (e.g. DialogRoute), ' + 'or when a generic type is explicitly provided to a route creation method ' + '(such as showDialog()) but the popped value does not match this type.', + ), + DiagnosticsProperty>('The route was', this), + DiagnosticsProperty('The provided result was', result), + ]); + } + return true; + } } /// Data that might be useful in constructing a [Route]. @@ -5565,7 +5587,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res return false; } assert(lastEntry.route._isInstalledIn(this)); - + assert(lastEntry.route._debugCheckCanConsumeResult(result, methodName: 'maybePop')); // TODO(justinmc): When the deprecated willPop method is removed, delete // this code and use only popDisposition, below. if (await lastEntry.route.willPop() == RoutePopDisposition.doNotPop) { @@ -5621,6 +5643,10 @@ class NavigatorState extends State with TickerProviderStateMixin, Res @optionalTypeArgs void pop([T? result]) { assert(!_debugLocked); + assert(() { + final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate); + return entry.route._debugCheckCanConsumeResult(result, methodName: 'pop'); + }()); assert(() { _debugLocked = true; return true; From 762181df463f9bc4a058e722858c2a74e4f3c9fb Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Fri, 15 May 2026 02:40:04 +0530 Subject: [PATCH 2/2] test: Add test for Navigator pop type mismatches Added a test case that reproduces the scenario from flutter#16987. Verifies that both pop and maybePop throw the new descriptive FlutterError with the correct diagnostic information. --- .../flutter/test/widgets/navigator_test.dart | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index da05c94651f4c..6aa591bdcc22a 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -6342,6 +6342,105 @@ void main() { }, variant: TargetPlatformVariant.only(TargetPlatform.iOS), ); + + testWidgets( + 'Navigator.pop and maybePop throw Flutter error when popped with mismatched type via showDialog', + experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), + (WidgetTester tester) async { + Object? popException; + Object? maybePopException; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + actions: [ + TextButton( + onPressed: () { + try { + Navigator.pop(context, 'NO'); + } catch (e) { + popException = e; + } + }, + child: const Text('NO'), + ), + TextButton( + onPressed: () { + Navigator.maybePop(context, 'YES').catchError((Object e) { + maybePopException = e; + return false; + }); + }, + child: const Text('YES'), + ), + ], + ); + }, + ); + }, + child: const Text('Open Dialog'), + ); + }, + ), + ), + ), + ); + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); + await tester.tap(find.text('NO')); + expect(popException, isFlutterError); + final popError = popException! as FlutterError; + expect( + popError.toStringDeep(), + equalsIgnoringHashCodes( + 'FlutterError\n' + ' A request was made to pop a route with a result of type String,\n' + ' but the route expected a value of type bool.\n' + ' This usually happens when the type provided to Navigator.pop() is\n' + ' not a subtype of the type expected by the Route (e.g.\n' + ' DialogRoute), or when a generic type is explicitly provided\n' + ' to a route creation method (such as showDialog()) but the\n' + ' popped value does not match this type.\n' + ' The route was: DialogRoute(RouteSettings(none, null),\n' + ' animation: AnimationController#00000(⏭ 1.000; paused; for\n' + ' DialogRoute))\n' + ' The provided result was: NO\n' + '', + ), + ); + + await tester.tap(find.text('YES')); + await tester.pumpAndSettle(); + expect(maybePopException, isFlutterError); + final maybePopError = maybePopException! as FlutterError; + expect( + maybePopError.toStringDeep(), + equalsIgnoringHashCodes( + 'FlutterError\n' + ' A request was made to pop a route with a result of type String,\n' + ' but the route expected a value of type bool.\n' + ' This usually happens when the type provided to\n' + ' Navigator.maybePop() is not a subtype of the type expected by the\n' + ' Route (e.g. DialogRoute), or when a generic type is\n' + ' explicitly provided to a route creation method (such as\n' + ' showDialog()) but the popped value does not match this type.\n' + ' The route was: DialogRoute(RouteSettings(none, null),\n' + ' animation: AnimationController#00000(⏭ 1.000; paused; for\n' + ' DialogRoute))\n' + ' The provided result was: YES\n' + '', + ), + ); + }, + ); } typedef AnnouncementCallBack = void Function(Route?);