Skip to content

Commit 2280b06

Browse files
mralephCommit Queue
authored and
Commit Queue
committed
[vm] Improve detection of handled async errors
Improve the handling of code where the async flow forks: e.g. for `fut.whenComplete(c.complete)` we might proceed unwinding through awaiters of `c.future` and forget about the future returned from `whenComplete`. This happens because we choose to present result of the unwinding as a single stack and not a tree. In this situation the error will only propagate into that future and whether or not the error will be handled depends on that future alone. As part of this change also start respecting ignored bit on futures without listeners. TEST=pkg/vm_service/test/pause_on_unhandled_async_exceptions7_test.dart CoreLibraryReviewExempt: Comment only changes to core library Change-Id: I27b689ab07a725e8faa8d91cf40e88ebc8c441a0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/352904 Reviewed-by: Martin Kustermann <[email protected]> Reviewed-by: Lasse Nielsen <[email protected]> Commit-Queue: Slava Egorov <[email protected]>
1 parent 2c11ad0 commit 2280b06

File tree

4 files changed

+119
-18
lines changed

4 files changed

+119
-18
lines changed

pkg/vm_service/test/pause_on_unhandled_async_exceptions7_test.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import 'pause_on_unhandled_async_exceptions6_test.dart' as test6;
2020

2121
Future<int> alwaysThrow() async {
2222
// Ensure that we suspend at least once and throw an error asynchronously.
23-
await Future.delayed(const Duration(milliseconds: 10));
23+
await Future.delayed(Duration.zero);
2424
print(StackTrace.current);
2525
throw 'Error';
2626
}
@@ -70,6 +70,24 @@ Future<void> throwSomeCaughtAsyncErrors() async {
7070
} finally {
7171
client.close();
7272
}
73+
74+
{
75+
print('ignoring an error (1)');
76+
final c = Completer<void>();
77+
// Here async unwinder might follow awaiter chain to `await c.future`
78+
// because it detects forwarding automatically.
79+
alwaysThrow().whenComplete(c.complete).ignore();
80+
await c.future;
81+
}
82+
83+
{
84+
print('ignoring an error (2)');
85+
final c = Completer<void>();
86+
alwaysThrow().whenComplete(() {
87+
c.complete();
88+
}).ignore();
89+
await c.future;
90+
}
7391
}
7492

7593
Future<void> testeeMain() async {

runtime/vm/stack_trace.cc

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const intptr_t k_StreamController__STATE_SUBSCRIPTION_MASK = 3;
2222
const intptr_t k_StreamController__STATE_ADDSTREAM = 8;
2323
// - _BufferingStreamSubscription._STATE_HAS_ERROR_HANDLER.
2424
const intptr_t k_BufferingStreamSubscription__STATE_HAS_ERROR_HANDLER = 1 << 5;
25+
// - _Future._stateIgnoreError
26+
const intptr_t k_Future__stateIgnoreError = 1;
2527
// - _FutureListener.stateThen.
2628
const intptr_t k_FutureListener_stateThen = 1;
2729
// - _FutureListener.stateCatchError.
@@ -79,31 +81,34 @@ bool TryGetAwaiterLink(const Closure& closure, Object* link) {
7981
// of awaiters.
8082
class AsyncAwareStackUnwinder : public ValueObject {
8183
public:
82-
explicit AsyncAwareStackUnwinder(Thread* thread)
84+
explicit AsyncAwareStackUnwinder(Thread* thread,
85+
bool* encountered_async_catch_error)
8386
: zone_(thread->zone()),
8487
sync_frames_(thread, StackFrameIterator::kNoCrossThreadIteration),
8588
sync_frame_(nullptr),
8689
awaiter_frame_{Closure::Handle(zone_), Object::Handle(zone_)},
90+
encountered_async_catch_error_(encountered_async_catch_error),
8791
closure_(Closure::Handle(zone_)),
8892
code_(Code::Handle(zone_)),
8993
context_(Context::Handle(zone_)),
9094
function_(Function::Handle(zone_)),
9195
parent_function_(Function::Handle(zone_)),
9296
object_(Object::Handle(zone_)),
97+
result_future_(Object::Handle(zone_)),
9398
suspend_state_(SuspendState::Handle(zone_)),
9499
controller_(Object::Handle(zone_)),
95100
subscription_(Object::Handle(zone_)),
96101
stream_iterator_(Object::Handle(zone_)),
97102
async_lib_(Library::Handle(zone_, Library::AsyncLibrary())),
98-
null_closure_(Closure::Handle(zone_)) {}
103+
null_closure_(Closure::Handle(zone_)) {
104+
if (encountered_async_catch_error_ != nullptr) {
105+
*encountered_async_catch_error_ = false;
106+
}
107+
}
99108

100109
void Unwind(intptr_t skip_frames,
101110
std::function<void(const StackTraceUtils::Frame&)> handle_frame);
102111

103-
bool encountered_async_catch_error() const {
104-
return encountered_async_catch_error_;
105-
}
106-
107112
private:
108113
bool HandleSynchronousFrame();
109114

@@ -137,6 +142,28 @@ class AsyncAwareStackUnwinder : public ValueObject {
137142

138143
ObjectPtr GetReceiver() const;
139144

145+
// Returns |true| if propagating an error to the listeners of this `_Future`
146+
// will always encounter an error handler. Future handles error iff:
147+
//
148+
// * It has no listeners and is marked as ignoring errors
149+
// * All of its listeners either have an error handler or corresponding
150+
// result future handles error.
151+
//
152+
// Note: this ignores simple error forwarding/rethrow which occurs in `await`
153+
// or patterns like `fut.then(onError: c.completeError)`.
154+
bool WillFutureHandleError(const Object& future, intptr_t depth = 0);
155+
156+
void MarkAsHandlingAsyncError() const {
157+
if (ShouldComputeIfAsyncErrorIsHandled()) {
158+
*encountered_async_catch_error_ = true;
159+
}
160+
}
161+
162+
bool ShouldComputeIfAsyncErrorIsHandled() const {
163+
return encountered_async_catch_error_ != nullptr &&
164+
!*encountered_async_catch_error_;
165+
}
166+
140167
#define USED_CLASS_LIST(V) \
141168
V(_AsyncStarStreamController) \
142169
V(_BufferingStreamSubscription) \
@@ -167,9 +194,11 @@ class AsyncAwareStackUnwinder : public ValueObject {
167194
V(_BufferingStreamSubscription, _state) \
168195
V(_Completer, future) \
169196
V(_Future, _resultOrListeners) \
197+
V(_Future, _state) \
170198
V(_FutureListener, callback) \
171199
V(_FutureListener, result) \
172200
V(_FutureListener, state) \
201+
V(_FutureListener, _nextListener) \
173202
V(_StreamController, _state) \
174203
V(_StreamController, _varData) \
175204
V(_StreamControllerAddStreamState, _varData) \
@@ -226,14 +255,15 @@ class AsyncAwareStackUnwinder : public ValueObject {
226255
StackFrame* sync_frame_;
227256
AwaiterFrame awaiter_frame_;
228257

229-
bool encountered_async_catch_error_ = false;
258+
bool* encountered_async_catch_error_;
230259

231260
Closure& closure_;
232261
Code& code_;
233262
Context& context_;
234263
Function& function_;
235264
Function& parent_function_;
236265
Object& object_;
266+
Object& result_future_;
237267
SuspendState& suspend_state_;
238268

239269
Object& controller_;
@@ -423,11 +453,47 @@ void AsyncAwareStackUnwinder::UnwindFrameToFutureListener() {
423453
if (object_.GetClassId() == _FutureListener().id()) {
424454
InitializeAwaiterFrameFromFutureListener(object_);
425455
} else {
456+
if (ShouldComputeIfAsyncErrorIsHandled()) {
457+
// Check if error on this future was ignored through |Future.ignore|.
458+
const auto state =
459+
Smi::Value(Smi::RawCast(Get_Future__state(awaiter_frame_.next)));
460+
if ((state & k_Future__stateIgnoreError) != 0) {
461+
MarkAsHandlingAsyncError();
462+
}
463+
}
464+
426465
awaiter_frame_.closure = Closure::null();
427466
awaiter_frame_.next = Object::null();
428467
}
429468
}
430469

470+
bool AsyncAwareStackUnwinder::WillFutureHandleError(const Object& future,
471+
intptr_t depth /* = 0 */) {
472+
if (depth > 100 || future.GetClassId() != _Future().id()) {
473+
return true; // Conservative.
474+
}
475+
476+
if (Get_Future__resultOrListeners(future) == Object::null()) {
477+
// No listeners: check if future is simply ignoring errors.
478+
const auto state = Smi::Value(Smi::RawCast(Get_Future__state(future)));
479+
return (state & k_Future__stateIgnoreError) != 0;
480+
}
481+
482+
for (auto& listener = Object::Handle(Get_Future__resultOrListeners(future));
483+
listener.GetClassId() == _FutureListener().id();
484+
listener = Get_FutureListener__nextListener(listener)) {
485+
const auto state =
486+
Smi::Value(Smi::RawCast(Get_FutureListener_state(listener)));
487+
if ((state & k_FutureListener_stateCatchError) == 0 &&
488+
!WillFutureHandleError(
489+
Object::Handle(Get_FutureListener_result(listener)), depth + 1)) {
490+
return false;
491+
}
492+
}
493+
494+
return true;
495+
}
496+
431497
void AsyncAwareStackUnwinder::InitializeAwaiterFrameFromFutureListener(
432498
const Object& listener) {
433499
if (listener.GetClassId() != _FutureListener().id()) {
@@ -445,15 +511,30 @@ void AsyncAwareStackUnwinder::InitializeAwaiterFrameFromFutureListener(
445511
(k_FutureListener_stateThen | k_FutureListener_stateCatchError) ||
446512
state == (k_FutureListener_stateThen | k_FutureListener_stateCatchError |
447513
k_FutureListener_maskAwait)) {
448-
awaiter_frame_.next = Get_FutureListener_result(listener);
514+
result_future_ = Get_FutureListener_result(listener);
449515
} else {
450-
awaiter_frame_.next = Object::null();
516+
result_future_ = Object::null();
451517
}
452518
awaiter_frame_.closure =
453519
Closure::RawCast(Get_FutureListener_callback(listener));
520+
awaiter_frame_.next = result_future_.ptr();
454521

455522
ComputeNextFrameFromAwaiterLink();
456523

524+
if (ShouldComputeIfAsyncErrorIsHandled() && !result_future_.IsNull() &&
525+
result_future_.ptr() != awaiter_frame_.next.ptr()) {
526+
// We have unwound through closure rather than followed result future, this
527+
// can be caused by unwinding through `await` or code like
528+
// `fut.whenComplete(c.complete)`. If the current future does not
529+
// catch the error then the error will be forwarded into result_future_.
530+
// Check if result_future_ handles it and set
531+
// encountered_async_catch_error_ respectively.
532+
if ((state & k_FutureListener_stateCatchError) == 0 &&
533+
WillFutureHandleError(result_future_)) {
534+
MarkAsHandlingAsyncError();
535+
}
536+
}
537+
457538
// If the Future has catchError callback attached through either
458539
// `Future.catchError` or `Future.then(..., onError: ...)` then we should
459540
// treat this listener as a catch all exception handler. However we should
@@ -468,7 +549,7 @@ void AsyncAwareStackUnwinder::InitializeAwaiterFrameFromFutureListener(
468549
if ((state & k_FutureListener_stateCatchError) != 0 &&
469550
((state & k_FutureListener_maskAwait) == 0 ||
470551
!awaiter_frame_.next.IsSuspendState())) {
471-
encountered_async_catch_error_ = true;
552+
MarkAsHandlingAsyncError();
472553
}
473554
}
474555

@@ -531,7 +612,7 @@ void AsyncAwareStackUnwinder::UnwindFrameToStreamController() {
531612
if (Get_StreamIterator__hasValue(stream_iterator_) ==
532613
Object::bool_true().ptr()) {
533614
if (has_error_handler) {
534-
encountered_async_catch_error_ = true;
615+
MarkAsHandlingAsyncError();
535616
}
536617
return;
537618
}
@@ -546,7 +627,7 @@ void AsyncAwareStackUnwinder::UnwindFrameToStreamController() {
546627
awaiter_frame_.next = object_.ptr();
547628
} else {
548629
if (has_error_handler) {
549-
encountered_async_catch_error_ = true;
630+
MarkAsHandlingAsyncError();
550631
}
551632
}
552633
return;
@@ -611,7 +692,7 @@ void AsyncAwareStackUnwinder::UnwindFrameToStreamController() {
611692

612693
if (has_error_handler && object_.GetClassId() != _Future().id() &&
613694
object_.GetClassId() != _SyncStreamController().id()) {
614-
encountered_async_catch_error_ = true;
695+
MarkAsHandlingAsyncError();
615696
}
616697

617698
if (found_awaiter_link_in_sibling_handler) {
@@ -666,11 +747,8 @@ void StackTraceUtils::CollectFrames(
666747
int skip_frames,
667748
const std::function<void(const StackTraceUtils::Frame&)>& handle_frame,
668749
bool* has_async_catch_error /* = null */) {
669-
AsyncAwareStackUnwinder it(thread);
750+
AsyncAwareStackUnwinder it(thread, has_async_catch_error);
670751
it.Unwind(skip_frames, handle_frame);
671-
if (has_async_catch_error != nullptr) {
672-
*has_async_catch_error = it.encountered_async_catch_error();
673-
}
674752
}
675753

676754
bool StackTraceUtils::GetSuspendState(const Closure& closure,

runtime/vm/symbols.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ class ObjectPointerVisitor;
454454
V(_mapGet, "_mapGet") \
455455
V(_mapKeys, "_mapKeys") \
456456
V(_name, "_name") \
457+
V(_nextListener, "_nextListener") \
457458
V(_nativeGetFloat32, "_nativeGetFloat32") \
458459
V(_nativeSetFloat32, "_nativeSetFloat32") \
459460
V(_nativeGetFloat64, "_nativeGetFloat64") \

sdk/lib/async/future_impl.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class _FutureListener<S, T> {
8888
maskValue | maskError | maskTestError | maskWhenComplete;
8989

9090
// Listeners on the same future are linked through this link.
91+
@pragma("vm:entry-point")
9192
_FutureListener? _nextListener;
9293

9394
// The future to complete when this listener is activated.
@@ -223,6 +224,8 @@ class _Future<T> implements Future<T> {
223224
/// Set by the [FutureExtensions.ignore] method to avoid
224225
/// having to introduce an unnecessary listener.
225226
/// Only relevant until the future is completed.
227+
///
228+
/// When changing update runtime/vm/stack_trace.cc
226229
static const int _stateIgnoreError = 1;
227230

228231
/// Pending completion. Set when completed using [_asyncComplete] or
@@ -254,6 +257,7 @@ class _Future<T> implements Future<T> {
254257
static const int _completionStateMask = 30;
255258

256259
/// Whether the future is complete, and as what.
260+
@pragma('vm:entry-point')
257261
int _state = _stateIncomplete;
258262

259263
/// Zone that the future was completed from.

0 commit comments

Comments
 (0)