More consistent unexpected error formatting (#2644)

- Include stack traces in the rejection output for more extensions which
  have catch blocks for unexpected exceptions.
- Use more consistent phrasing and formatting with a line ending in
  `at:` before the stack trace.
- Add indentation to all outputs that include stack traces.
- Use the `LineSplitter.split` utility method everywhere lines are split.
diff --git a/pkgs/checks/CHANGELOG.md b/pkgs/checks/CHANGELOG.md
index 834d180..27d8623 100644
--- a/pkgs/checks/CHANGELOG.md
+++ b/pkgs/checks/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 - Require Dart 3.7
 - Improve speed of pretty printing for large collections.
+- Improve formatting for failures involving unexpected exceptions.
 
 ## 0.3.1
 
diff --git a/pkgs/checks/lib/src/describe.dart b/pkgs/checks/lib/src/describe.dart
index 88b46d9..f5cf7a5 100644
--- a/pkgs/checks/lib/src/describe.dart
+++ b/pkgs/checks/lib/src/describe.dart
@@ -76,7 +76,7 @@
   } else if (object is Condition<Never>) {
     return ['<A value that:', ...postfixLast('>', describe(object))];
   } else {
-    final value = const LineSplitter().convert(object.toString());
+    final value = LineSplitter.split(object.toString());
     return isTopLevel ? prefixFirst('<', postfixLast('>', value)) : value;
   }
 }
diff --git a/pkgs/checks/lib/src/extensions/async.dart b/pkgs/checks/lib/src/extensions/async.dart
index 34a768b..2ff8ace 100644
--- a/pkgs/checks/lib/src/extensions/async.dart
+++ b/pkgs/checks/lib/src/extensions/async.dart
@@ -27,7 +27,7 @@
           actual: ['a future that completes as an error'],
           which: [
             ...prefixFirst('threw ', postfixLast(' at:', literal(e))),
-            ...const LineSplitter().convert(st.toString()),
+            ...indent(LineSplitter.split(st.toString())),
           ],
         );
       }
@@ -57,10 +57,10 @@
           onError: (Object e, StackTrace st) {
             reject(
               Rejection(
-                actual: ['a future that completed as an error:'],
+                actual: ['a future that completed as an error'],
                 which: [
-                  ...prefixFirst('threw ', literal(e)),
-                  ...const LineSplitter().convert(st.toString()),
+                  ...prefixFirst('threw ', postfixLast(' at:', literal(e))),
+                  ...indent(LineSplitter.split(st.toString())),
                 ],
               ),
             );
@@ -97,7 +97,7 @@
             actual: prefixFirst('completed to error ', literal(e)),
             which: [
               'threw an exception that is not a $E at:',
-              ...const LineSplitter().convert(st.toString()),
+              ...indent(LineSplitter.split(st.toString())),
             ],
           );
         }
@@ -160,7 +160,7 @@
           actual: prefixFirst('a stream with error ', literal(e)),
           which: [
             'emitted an error instead of a value at:',
-            ...const LineSplitter().convert(st.toString()),
+            ...indent(LineSplitter.split(st.toString())),
           ],
         );
       }
@@ -208,7 +208,7 @@
             actual: prefixFirst('a stream with error ', literal(e)),
             which: [
               'emitted an error which is not $E at:',
-              ...const LineSplitter().convert(st.toString()),
+              ...indent(LineSplitter.split(st.toString())),
             ],
           );
         }
@@ -510,8 +510,11 @@
         return Rejection(
           actual: ['a stream'],
           which: [
-            ...prefixFirst('emitted an unexpected error: ', literal(e)),
-            ...const LineSplitter().convert(st.toString()),
+            ...prefixFirst(
+              'emitted an unexpected error: ',
+              postfixLast(' at:', literal(e)),
+            ),
+            ...indent(LineSplitter.split(st.toString())),
           ],
         );
       }
diff --git a/pkgs/checks/lib/src/extensions/core.dart b/pkgs/checks/lib/src/extensions/core.dart
index 8059553..9f3dd73 100644
--- a/pkgs/checks/lib/src/extensions/core.dart
+++ b/pkgs/checks/lib/src/extensions/core.dart
@@ -42,8 +42,11 @@
       } catch (e, st) {
         return Extracted.rejection(
           which: [
-            ...prefixFirst('threw while trying to read $name: ', literal(e)),
-            ...const LineSplitter().convert(st.toString()),
+            ...prefixFirst(
+              'threw while trying to read $name: ',
+              postfixLast(' at:', literal(e)),
+            ),
+            ...indent(LineSplitter.split(st.toString())),
           ],
         );
       }
diff --git a/pkgs/checks/lib/src/extensions/function.dart b/pkgs/checks/lib/src/extensions/function.dart
index c23d4a6..0fb27f3 100644
--- a/pkgs/checks/lib/src/extensions/function.dart
+++ b/pkgs/checks/lib/src/extensions/function.dart
@@ -1,6 +1,7 @@
 // Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+import 'dart:convert';
 
 import '../../context.dart';
 
@@ -24,11 +25,14 @@
           actual: prefixFirst('a function that returned ', literal(result)),
           which: ['did not throw'],
         );
-      } catch (e) {
+      } catch (e, st) {
         if (e is E) return Extracted.value(e as E);
         return Extracted.rejection(
           actual: prefixFirst('a function that threw error ', literal(e)),
-          which: ['did not throw an $E'],
+          which: [
+            'threw an exception that is not a $E at:',
+            ...indent(LineSplitter.split(st.toString())),
+          ],
         );
       }
     });
@@ -48,8 +52,8 @@
         return Extracted.rejection(
           actual: ['a function that throws'],
           which: [
-            ...prefixFirst('threw ', literal(e)),
-            ...st.toString().split('\n'),
+            ...prefixFirst('threw ', postfixLast(' at:', literal(e))),
+            ...indent(LineSplitter.split(st.toString())),
           ],
         );
       }
diff --git a/pkgs/checks/test/context_test.dart b/pkgs/checks/test/context_test.dart
index d7c5567..064dd39 100644
--- a/pkgs/checks/test/context_test.dart
+++ b/pkgs/checks/test/context_test.dart
@@ -171,7 +171,7 @@
           Rejection(
             which: [
               ...prefixFirst('threw late error', literal(error.error)),
-              ...const LineSplitter().convert(
+              ...LineSplitter.split(
                 TestHandle.current
                     .formatStackTrace(error.stackTrace)
                     .toString(),
diff --git a/pkgs/checks/test/extensions/async_test.dart b/pkgs/checks/test/extensions/async_test.dart
index e632bc4..75647be 100644
--- a/pkgs/checks/test/extensions/async_test.dart
+++ b/pkgs/checks/test/extensions/async_test.dart
@@ -22,7 +22,7 @@
         await check(_futureFail()).isRejectedByAsync(
           (it) => it.completes((it) => it.equals(1)),
           actual: ['a future that completes as an error'],
-          which: ['threw <UnimplementedError> at:', 'fake trace'],
+          which: ['threw <UnimplementedError> at:', '  fake trace'],
         );
       });
       test('can be described', () async {
@@ -66,7 +66,7 @@
             actual: ['completed to error <UnimplementedError>'],
             which: [
               'threw an exception that is not a StateError at:',
-              'fake trace',
+              '  fake trace',
             ],
           );
         },
@@ -134,9 +134,9 @@
             .equals('''
 Expected: a Future<String> that:
   does not complete
-Actual: a future that completed as an error:
-Which: threw 'error'
-fake trace''');
+Actual: a future that completed as an error
+Which: threw 'error' at:
+  fake trace''');
       });
       test('can be described', () async {
         await check(
@@ -164,7 +164,7 @@
         await check(_countingStream(1, errorAt: 0)).isRejectedByAsync(
           (it) => it.emits(),
           actual: ['a stream with error <UnimplementedError: Error at 1>'],
-          which: ['emitted an error instead of a value at:', 'fake trace'],
+          which: ['emitted an error instead of a value at:', '  fake trace'],
         );
       });
       test('can be described', () async {
@@ -215,7 +215,7 @@
             actual: ['a stream with error <UnimplementedError: Error at 1>'],
             which: [
               'emitted an error which is not StateError at:',
-              'fake trace',
+              '  fake trace',
             ],
           );
         },
@@ -494,7 +494,7 @@
         await check(StreamQueue(controller.stream)).isRejectedByAsync(
           (it) => it.isDone(),
           actual: ['a stream'],
-          which: ['emitted an unexpected error: \'sad\'', 'fake trace'],
+          which: ['emitted an unexpected error: \'sad\' at:', '  fake trace'],
         );
       });
       test('uses a transaction', () async {
diff --git a/pkgs/checks/test/extensions/core_test.dart b/pkgs/checks/test/extensions/core_test.dart
index cbcca7b..5a44da8 100644
--- a/pkgs/checks/test/extensions/core_test.dart
+++ b/pkgs/checks/test/extensions/core_test.dart
@@ -27,8 +27,8 @@
           );
         }, 'foo').isNotNull(),
         which: [
-          'threw while trying to read foo: <UnimplementedError>',
-          'fake trace',
+          'threw while trying to read foo: <UnimplementedError> at:',
+          '  fake trace',
         ],
       );
     });
diff --git a/pkgs/checks/test/extensions/function_test.dart b/pkgs/checks/test/extensions/function_test.dart
index aefba11..dde1600 100644
--- a/pkgs/checks/test/extensions/function_test.dart
+++ b/pkgs/checks/test/extensions/function_test.dart
@@ -21,10 +21,18 @@
         );
       });
       test('fails for functions that throw the wrong type', () {
-        check(() => throw StateError('oops!')).isRejectedBy(
+        check(() {
+          Error.throwWithStackTrace(
+            StateError('oops!'),
+            StackTrace.fromString('fake trace'),
+          );
+        }).isRejectedBy(
           (it) => it.throws<ArgumentError>(),
           actual: ['a function that threw error <Bad state: oops!>'],
-          which: ['did not throw an ArgumentError'],
+          which: [
+            'threw an exception that is not a ArgumentError at:',
+            '  fake trace',
+          ],
         );
       });
     });
@@ -42,7 +50,7 @@
         }).isRejectedBy(
           (it) => it.returnsNormally(),
           actual: ['a function that throws'],
-          which: ['threw <Bad state: oops!>', 'fake trace'],
+          which: ['threw <Bad state: oops!> at:', '  fake trace'],
         );
       });
     });