Avoid printing blank lines between "Another exception was thrown:" messages. (#119587)

diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart
index 9c707d4..304b406 100644
--- a/dev/bots/analyze.dart
+++ b/dev/bots/analyze.dart
@@ -169,8 +169,9 @@
 
   // Try with the --watch analyzer, to make sure it returns success also.
   // The --benchmark argument exits after one run.
+  // We specify a failureMessage so that the actual output is muted in the case where _runFlutterAnalyze above already failed.
   printProgress('Dart analysis (with --watch)...');
-  await _runFlutterAnalyze(flutterRoot, options: <String>[
+  await _runFlutterAnalyze(flutterRoot, failureMessage: 'Dart analyzer failed when --watch was used.', options: <String>[
     '--flutter-repo',
     '--watch',
     '--benchmark',
@@ -196,7 +197,7 @@
       ],
       workingDirectory: flutterRoot,
     );
-    await _runFlutterAnalyze(outDir.path, options: <String>[
+    await _runFlutterAnalyze(outDir.path, failureMessage: 'Dart analyzer failed on mega_gallery benchmark.', options: <String>[
       '--watch',
       '--benchmark',
       ...arguments,
@@ -1942,11 +1943,13 @@
 
 Future<CommandResult> _runFlutterAnalyze(String workingDirectory, {
   List<String> options = const <String>[],
+  String? failureMessage,
 }) async {
   return runCommand(
     flutter,
     <String>['analyze', ...options],
     workingDirectory: workingDirectory,
+    failureMessage: failureMessage,
   );
 }
 
diff --git a/dev/bots/run_command.dart b/dev/bots/run_command.dart
index 0a013be..a58d5be 100644
--- a/dev/bots/run_command.dart
+++ b/dev/bots/run_command.dart
@@ -190,13 +190,24 @@
         print(result.flattenedStderr);
         break;
     }
+    String allOutput;
+    if (failureMessage == null) {
+      allOutput = '${result.flattenedStdout}\n${result.flattenedStderr}';
+      if (allOutput.split('\n').length > 10) {
+        allOutput = '(stdout/stderr output was more than 10 lines)';
+      }
+    } else {
+      allOutput = '';
+    }
     foundError(<String>[
       if (failureMessage != null)
-        failureMessage
-      else
-        '$bold${red}Command exited with exit code ${result.exitCode} but expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'} exit code.$reset',
+        failureMessage,
       '${bold}Command: $green$commandDescription$reset',
-      '${bold}Relative working directory: $cyan$relativeWorkingDir$reset',
+      if (failureMessage == null)
+        '$bold${red}Command exited with exit code ${result.exitCode} but expected ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'} exit code.$reset',
+      '${bold}Working directory: $cyan${path.absolute(relativeWorkingDir)}$reset',
+      if (allOutput.isNotEmpty)
+        '${bold}stdout and stderr output:\n$allOutput',
     ]);
   } else {
     print('ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
diff --git a/dev/bots/utils.dart b/dev/bots/utils.dart
index f94c475..91d744d 100644
--- a/dev/bots/utils.dart
+++ b/dev/bots/utils.dart
@@ -82,8 +82,7 @@
 bool get hasError => _hasError;
 bool _hasError = false;
 
-Iterable<String> get errorMessages => _errorMessages;
-List<String> _errorMessages = <String>[];
+List<List<String>> _errorMessages = <List<String>>[];
 
 final List<String> _pendingLogs = <String>[];
 Timer? _hideTimer; // When this is null, the output is verbose.
@@ -104,7 +103,7 @@
   // another error.
   _pendingLogs.forEach(_printLoudly);
   _pendingLogs.clear();
-  _errorMessages.addAll(messages);
+  _errorMessages.add(messages);
   _hasError = true;
   if (onError != null) {
     onError!();
@@ -124,8 +123,18 @@
   _hideTimer?.cancel();
   _hideTimer = null;
   print(redLine);
-  print('For your convenience, the error messages reported above are repeated here:');
-  _errorMessages.forEach(print);
+  print('${red}For your convenience, the error messages reported above are repeated here:$reset');
+  final bool printSeparators = _errorMessages.any((List<String> messages) => messages.length > 1);
+  if (printSeparators) {
+    print('  🙙  🙛  ');
+  }
+  for (int index = 0; index < _errorMessages.length * 2 - 1; index += 1) {
+    if (index.isEven) {
+      _errorMessages[index ~/ 2].forEach(print);
+    } else if (printSeparators) {
+      print('  🙙  🙛  ');
+    }
+  }
   print(redLine);
   system.exit(1);
 }
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index dccdd2a..78707b7 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -1329,9 +1329,25 @@
 
   void printStructuredErrorLog(vm_service.Event event) {
     if (event.extensionKind == 'Flutter.Error' && !machine) {
-      final Map<dynamic, dynamic>? json = event.extensionData?.data;
+      final Map<String, Object?>? json = event.extensionData?.data;
       if (json != null && json.containsKey('renderedErrorText')) {
-        globals.printStatus('\n${json['renderedErrorText']}');
+        final int errorsSinceReload;
+        if (json.containsKey('errorsSinceReload') && json['errorsSinceReload'] is int) {
+          errorsSinceReload = json['errorsSinceReload']! as int;
+        } else {
+          errorsSinceReload = 0;
+        }
+        if (errorsSinceReload == 0) {
+          // We print a blank line around the first error, to more clearly emphasize it
+          // in the output. (Other errors don't get this.)
+          globals.printStatus('');
+        }
+        globals.printStatus('${json['renderedErrorText']}');
+        if (errorsSinceReload == 0) {
+          globals.printStatus('');
+        }
+      } else {
+        globals.printError('Received an invalid ${globals.logger.terminal.bolden("Flutter.Error")} message from app: $json');
       }
     }
   }
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index a6e177a..50d62ff 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -285,11 +285,11 @@
   }
   if (printStructuredErrorLogMethod != null) {
     vmService.onExtensionEvent.listen(printStructuredErrorLogMethod);
-    // It is safe to ignore this error because we expect an error to be
-    // thrown if we're already subscribed.
     registrationRequests.add(vmService
       .streamListen(vm_service.EventStreams.kExtension)
       .then<vm_service.Success?>((vm_service.Success success) => success)
+      // It is safe to ignore this error because we expect an error to be
+      // thrown if we're already subscribed.
       .catchError((Object? error) => null, test: (Object? error) => error is vm_service.RPCError)
     );
   }
diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
index c5baa2b..47b91fb 100644
--- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
@@ -429,25 +429,69 @@
       () async {
     final ResidentRunner residentWebRunner =
         setUpResidentRunner(flutterDevice, logger: testLogger);
-    final Map<String, String> extensionData = <String, String>{
-      'test': 'data',
-      'renderedErrorText': 'error text',
-    };
-    final Map<String, String> emptyExtensionData = <String, String>{
-      'test': 'data',
-      'renderedErrorText': '',
-    };
-    final Map<String, String> nonStructuredErrorData = <String, String>{
-      'other': 'other stuff',
-    };
-    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
+    final List<VmServiceExpectation> requests = <VmServiceExpectation>[
       ...kAttachExpectations,
+      // This is the first error message of a session.
       FakeVmServiceStreamResponse(
         streamId: 'Extension',
         event: vm_service.Event(
           timestamp: 0,
           extensionKind: 'Flutter.Error',
-          extensionData: vm_service.ExtensionData.parse(extensionData),
+          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
+            'errorsSinceReload': 0,
+            'renderedErrorText': 'first',
+          }),
+          kind: vm_service.EventStreams.kExtension,
+        ),
+      ),
+      // This is the second error message of a session.
+      FakeVmServiceStreamResponse(
+        streamId: 'Extension',
+        event: vm_service.Event(
+          timestamp: 0,
+          extensionKind: 'Flutter.Error',
+          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
+            'errorsSinceReload': 1,
+            'renderedErrorText': 'second',
+          }),
+          kind: vm_service.EventStreams.kExtension,
+        ),
+      ),
+      // This is not Flutter.Error kind data, so it should not be logged, even though it has a renderedErrorText field.
+      FakeVmServiceStreamResponse(
+        streamId: 'Extension',
+        event: vm_service.Event(
+          timestamp: 0,
+          extensionKind: 'Other',
+          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
+            'errorsSinceReload': 2,
+            'renderedErrorText': 'not an error',
+          }),
+          kind: vm_service.EventStreams.kExtension,
+        ),
+      ),
+      // This is the third error message of a session.
+      FakeVmServiceStreamResponse(
+        streamId: 'Extension',
+        event: vm_service.Event(
+          timestamp: 0,
+          extensionKind: 'Flutter.Error',
+          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
+            'errorsSinceReload': 2,
+            'renderedErrorText': 'third',
+          }),
+          kind: vm_service.EventStreams.kExtension,
+        ),
+      ),
+      // This is bogus error data.
+      FakeVmServiceStreamResponse(
+        streamId: 'Extension',
+        event: vm_service.Event(
+          timestamp: 0,
+          extensionKind: 'Flutter.Error',
+          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
+            'other': 'bad stuff',
+          }),
           kind: vm_service.EventStreams.kExtension,
         ),
       ),
@@ -457,21 +501,33 @@
         event: vm_service.Event(
           timestamp: 0,
           extensionKind: 'Flutter.Error',
-          extensionData: vm_service.ExtensionData.parse(emptyExtensionData),
+          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
+            'test': 'data',
+            'renderedErrorText': '',
+          }),
           kind: vm_service.EventStreams.kExtension,
         ),
       ),
-      // This is not Flutter.Error kind data, so it should not be logged.
+      // Messages without errorsSinceReload should act as if errorsSinceReload: 0
       FakeVmServiceStreamResponse(
         streamId: 'Extension',
         event: vm_service.Event(
           timestamp: 0,
-          extensionKind: 'Other',
-          extensionData: vm_service.ExtensionData.parse(nonStructuredErrorData),
+          extensionKind: 'Flutter.Error',
+          extensionData: vm_service.ExtensionData.parse(<String, Object?>{
+            'test': 'data',
+            'renderedErrorText': 'error text',
+          }),
           kind: vm_service.EventStreams.kExtension,
         ),
       ),
-    ]);
+      // When adding things here, make sure the last one is supposed to output something
+      // to the statusLog, otherwise you won't be able to distinguish the absence of output
+      // due to it passing the test from absence due to it not running the test.
+    ];
+    // We use requests below, so make a copy of it here (FakeVmServiceHost will
+    // clear its copy internally, which would affect our pumping below).
+    fakeVmServiceHost = FakeVmServiceHost(requests: requests.toList());
 
     setupMocks();
     final Completer<DebugConnectionInfo> connectionInfoCompleter =
@@ -480,10 +536,34 @@
       connectionInfoCompleter: connectionInfoCompleter,
     ));
     await connectionInfoCompleter.future;
-    await null;
+    assert(requests.length > 5, 'requests was modified');
+    for (final Object _ in requests) {
+      // pump the task queue once for each message
+      await null;
+    }
 
-    expect(testLogger.statusText, contains('\nerror text'));
-    expect(testLogger.statusText, isNot(contains('other stuff')));
+    expect(testLogger.statusText,
+      'Launching lib/main.dart on FakeDevice in debug mode...\n'
+      'Waiting for connection from debug service on FakeDevice...\n'
+      'Debug service listening on ws://127.0.0.1/abcd/\n'
+      '\n'
+      '💪 Running with sound null safety 💪\n'
+      '\n'
+      'first\n'
+      '\n'
+      'second\n'
+      'third\n'
+      '\n'
+      '\n' // the empty message
+      '\n'
+      '\n'
+      'error text\n'
+      '\n'
+    );
+
+    expect(testLogger.errorText,
+      'Received an invalid Flutter.Error message from app: {other: bad stuff}\n'
+    );
   }, overrides: <Type, Generator>{
     FileSystem: () => fileSystem,
     ProcessManager: () => processManager,
diff --git a/packages/flutter_tools/test/integration.shard/overall_experience_test.dart b/packages/flutter_tools/test/integration.shard/overall_experience_test.dart
index 6df9683..6a4d8ed 100644
--- a/packages/flutter_tools/test/integration.shard/overall_experience_test.dart
+++ b/packages/flutter_tools/test/integration.shard/overall_experience_test.dart
@@ -548,6 +548,7 @@
         '  verticalDirection: down',
         '◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤',
         '════════════════════════════════════════════════════════════════════════════════════════════════════',
+        '',
         startsWith('Reloaded 0 libraries in '),
         '',
         'Application finished.',