Remove duplicate code in Element.rebuild() and BuildOwner.buildScope() (#71940)

Element.rebuild() and BuildOwner.buildScope() both had code to
manager the `BuildOwner._debugCurrentBuildTarget` state variable.
Unfortunately, only one of the two places was using a try/finally
to ensure that the state variable was restored even if an error
was thrown.

This creates a private method to unify the copy/pasted code.
diff --git a/packages/flutter/lib/src/gestures/pointer_router.dart b/packages/flutter/lib/src/gestures/pointer_router.dart
index daf09bf..a59d07d 100644
--- a/packages/flutter/lib/src/gestures/pointer_router.dart
+++ b/packages/flutter/lib/src/gestures/pointer_router.dart
@@ -75,6 +75,7 @@
   ///
   /// This is valid in debug builds only. In release builds, this will throw an
   /// [UnsupportedError].
+  @visibleForTesting
   int get debugGlobalRouteCount {
     int? count;
     assert(() {
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index d2d4ed2..73855e2 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -2651,6 +2651,26 @@
   /// Only valid when asserts are enabled.
   bool get debugBuilding => _debugBuilding;
   bool _debugBuilding = false;
+
+  /// The element currently being built, or null if no element is actively
+  /// being built.
+  ///
+  /// This is valid in debug builds only. In release builds, this will throw an
+  /// [UnsupportedError].
+  @visibleForTesting
+  Element? get debugCurrentBuildTarget {
+    Element? result;
+    bool isSupportedOperation = false;
+    assert(() {
+      result = _debugCurrentBuildTarget;
+      isSupportedOperation = true;
+      return true;
+    }());
+    if (isSupportedOperation) {
+      return result;
+    }
+    throw UnsupportedError('debugCurrentBuildTarget is not supported in release builds');
+  }
   Element? _debugCurrentBuildTarget;
 
   /// Establishes a scope in which calls to [State.setState] are forbidden, and
@@ -2676,6 +2696,25 @@
     assert(_debugStateLockLevel >= 0);
   }
 
+  void _runWithCurrentBuildTarget(Element element, VoidCallback callback) {
+    assert(_debugStateLocked);
+    Element? debugPreviousBuildTarget;
+    assert(() {
+      debugPreviousBuildTarget = _debugCurrentBuildTarget;
+      _debugCurrentBuildTarget = element;
+      return true;
+    }());
+    try {
+      callback();
+    } finally {
+      assert(() {
+        assert(_debugCurrentBuildTarget == element);
+        _debugCurrentBuildTarget = debugPreviousBuildTarget;
+        return true;
+      }());
+    }
+  }
+
   /// Establishes a scope for updating the widget tree, and calls the given
   /// `callback`, if any. Then, builds all the elements that were marked as
   /// dirty using [scheduleBuildFor], in depth order.
@@ -2718,26 +2757,22 @@
     try {
       _scheduledFlushDirtyElements = true;
       if (callback != null) {
-        assert(_debugStateLocked);
-        Element? debugPreviousBuildTarget;
-        assert(() {
-          context._debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
-          debugPreviousBuildTarget = _debugCurrentBuildTarget;
-          _debugCurrentBuildTarget = context;
-          return true;
-        }());
-        _dirtyElementsNeedsResorting = false;
-        try {
-          callback();
-        } finally {
+        _runWithCurrentBuildTarget(context, () {
           assert(() {
-            context._debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
-            assert(_debugCurrentBuildTarget == context);
-            _debugCurrentBuildTarget = debugPreviousBuildTarget;
-            _debugElementWasRebuilt(context);
+            context._debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
             return true;
           }());
-        }
+          _dirtyElementsNeedsResorting = false;
+          try {
+            callback();
+          } finally {
+            assert(() {
+              context._debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
+              _debugElementWasRebuilt(context);
+              return true;
+            }());
+          }
+        });
       }
       _dirtyElements.sort(Element._sort);
       _dirtyElementsNeedsResorting = false;
@@ -4369,19 +4404,7 @@
       return true;
     }());
     assert(_lifecycleState == _ElementLifecycle.active);
-    assert(owner!._debugStateLocked);
-    Element? debugPreviousBuildTarget;
-    assert(() {
-      debugPreviousBuildTarget = owner!._debugCurrentBuildTarget;
-      owner!._debugCurrentBuildTarget = this;
-      return true;
-    }());
-    performRebuild();
-    assert(() {
-      assert(owner!._debugCurrentBuildTarget == this);
-      owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
-      return true;
-    }());
+    owner!._runWithCurrentBuildTarget(this, performRebuild);
     assert(!_dirty);
   }
 
@@ -5586,6 +5609,7 @@
   }
 
   @override
+  @mustCallSuper
   void performRebuild() {
     assert(() {
       _debugDoingBuild = true;
diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart
index 7dc9e7e..b213f94 100644
--- a/packages/flutter/test/widgets/framework_test.dart
+++ b/packages/flutter/test/widgets/framework_test.dart
@@ -1494,6 +1494,44 @@
     expect(GestureBinding.instance!.pointerRouter.debugGlobalRouteCount, pointerRouterCount);
     expect(RawKeyboard.instance.keyEventHandler, same(rawKeyEventHandler));
   });
+
+  testWidgets('Errors in build', (WidgetTester tester) async {
+    final ErrorWidgetBuilder oldErrorBuilder = ErrorWidget.builder;
+    ErrorWidget.builder = (FlutterErrorDetails detail) => throw AssertionError();
+    try {
+      final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
+      FlutterError.onError = (FlutterErrorDetails detail) {};
+      try {
+        int buildCount = 0;
+        void handleBuild() => buildCount++;
+        expect(tester.binding.buildOwner!.debugCurrentBuildTarget, isNull);
+        await tester.pumpWidget(_WidgetThatCanThrowInBuild(onBuild: handleBuild));
+        expect(buildCount, 1);
+        await tester.pumpWidget(_WidgetThatCanThrowInBuild(onBuild: handleBuild, shouldThrow: true));
+        tester.takeException();
+        expect(buildCount, 2);
+        expect(tester.binding.buildOwner!.debugCurrentBuildTarget, isNull);
+      } finally {
+        FlutterError.onError = oldErrorHandler;
+      }
+    } finally {
+      ErrorWidget.builder = oldErrorBuilder;
+    }
+  });
+}
+
+class _WidgetThatCanThrowInBuild extends StatelessWidget {
+  const _WidgetThatCanThrowInBuild({required this.onBuild, this.shouldThrow = false});
+
+  final VoidCallback onBuild;
+  final bool shouldThrow;
+
+  @override
+  Widget build(BuildContext context) {
+    onBuild();
+    assert(!shouldThrow);
+    return Container();
+  }
 }
 
 class _FakeFocusManager implements FocusManager {