Skip to content

Commit c1ccca2

Browse files
authored
Include stack for unexpected async errors (#1890)
When a Future or Stream throw (or emit) an unexpected exception it is useful to know where originated.
1 parent 505c309 commit c1ccca2

File tree

3 files changed

+38
-17
lines changed

3 files changed

+38
-17
lines changed

pkgs/checks/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# 0.1.1-dev
22

3-
- Added an example.
3+
- Added an example.
4+
- Include a stack trace in the failure description for unexpected errors from
5+
Futures or Streams.
46

57
# 0.1.0
68

pkgs/checks/lib/src/extensions/async.dart

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ extension FutureChecks<T> on Subject<Future<T>> {
1919
context.nestAsync<T>('completes to a value', (actual) async {
2020
try {
2121
return Extracted.value(await actual);
22-
} catch (e) {
23-
return Extracted.rejection(
24-
actual: ['a future that completes as an error'],
25-
which: prefixFirst('threw ', literal(e)));
22+
} catch (e, st) {
23+
return Extracted.rejection(actual: [
24+
'a future that completes as an error'
25+
], which: [
26+
...prefixFirst('threw ', postfixLast(' at:', literal(e))),
27+
...(const LineSplitter()).convert(st.toString())
28+
]);
2629
}
2730
});
2831

@@ -66,10 +69,13 @@ extension FutureChecks<T> on Subject<Future<T>> {
6669
which: ['did not throw']);
6770
} on E catch (e) {
6871
return Extracted.value(e);
69-
} catch (e) {
72+
} catch (e, st) {
7073
return Extracted.rejection(
7174
actual: prefixFirst('completed to error ', literal(e)),
72-
which: ['is not an $E']);
75+
which: [
76+
'threw an exception that is not a $E at:',
77+
...(const LineSplitter()).convert(st.toString())
78+
]);
7379
}
7480
});
7581
}
@@ -113,10 +119,13 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
113119
try {
114120
await actual.peek;
115121
return Extracted.value(await actual.next);
116-
} catch (e) {
122+
} catch (e, st) {
117123
return Extracted.rejection(
118124
actual: prefixFirst('a stream with error ', literal(e)),
119-
which: ['emitted an error instead of a value']);
125+
which: [
126+
'emitted an error instead of a value at:',
127+
...(const LineSplitter()).convert(st.toString())
128+
]);
120129
}
121130
});
122131

@@ -147,10 +156,13 @@ extension StreamChecks<T> on Subject<StreamQueue<T>> {
147156
} on E catch (e) {
148157
await actual.next.then<void>((_) {}, onError: (_) {});
149158
return Extracted.value(e);
150-
} catch (e) {
159+
} catch (e, st) {
151160
return Extracted.rejection(
152161
actual: prefixFirst('a stream with error ', literal(e)),
153-
which: ['emitted an error with an incorrect type, is not $E']);
162+
which: [
163+
'emitted an error which is not $E at:',
164+
...(const LineSplitter()).convert(st.toString())
165+
]);
154166
}
155167
});
156168

pkgs/checks/test/extensions/async_test.dart

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ void main() {
2222
await checkThat(_futureFail()).isRejectedByAsync(
2323
it()..completes().which(it()..equals(1)),
2424
actual: ['a future that completes as an error'],
25-
which: ['threw <UnimplementedError>'],
25+
which: ['threw <UnimplementedError> at:', 'fake trace'],
2626
);
2727
});
2828
test('can be described', () async {
@@ -59,7 +59,10 @@ void main() {
5959
await checkThat(_futureFail()).isRejectedByAsync(
6060
it()..throws<StateError>(),
6161
actual: ['completed to error <UnimplementedError>'],
62-
which: ['is not an StateError'],
62+
which: [
63+
'threw an exception that is not a StateError at:',
64+
'fake trace'
65+
],
6366
);
6467
});
6568
test('can be described', () async {
@@ -141,7 +144,7 @@ fake trace''');
141144
await checkThat(_countingStream(1, errorAt: 0)).isRejectedByAsync(
142145
it()..emits(),
143146
actual: ['a stream with error <UnimplementedError: Error at 1>'],
144-
which: ['emitted an error instead of a value'],
147+
which: ['emitted an error instead of a value at:', 'fake trace'],
145148
);
146149
});
147150
test('can be described', () async {
@@ -188,7 +191,7 @@ fake trace''');
188191
await checkThat(_countingStream(1, errorAt: 0)).isRejectedByAsync(
189192
it()..emitsError<StateError>(),
190193
actual: ['a stream with error <UnimplementedError: Error at 1>'],
191-
which: ['emitted an error with an incorrect type, is not StateError'],
194+
which: ['emitted an error which is not StateError at:', 'fake trace'],
192195
);
193196
});
194197
test('can be described', () async {
@@ -529,12 +532,16 @@ fake trace''');
529532

530533
Future<int> _futureSuccess() => Future.microtask(() => 42);
531534

532-
Future<int> _futureFail() => Future.error(UnimplementedError());
535+
Future<int> _futureFail() =>
536+
Future.error(UnimplementedError(), StackTrace.fromString('fake trace'));
533537

534538
StreamQueue<int> _countingStream(int count, {int? errorAt}) => StreamQueue(
535539
Stream.fromIterable(
536540
Iterable<int>.generate(count, (index) {
537-
if (index == errorAt) throw UnimplementedError('Error at $count');
541+
if (index == errorAt) {
542+
Error.throwWithStackTrace(UnimplementedError('Error at $count'),
543+
StackTrace.fromString('fake trace'));
544+
}
538545
return index;
539546
}),
540547
),

0 commit comments

Comments
 (0)