mirror of
https://github.com/dart-lang/sdk
synced 2024-10-14 15:21:54 +00:00
[VM] Adds Future.wait support for --lazy-async-stacks.
- Makes Future.wait a recognised function, and asserts its chained future, _future is allocated at a known index in the context. - Adds logic to locate, extract the chained future during lazy async stack unwinding. - Adds tests for the Future.wait async case. - Minor consistency nits, comments. This change is similar to a previous CL, adding Future.timeout support: https://dart-review.googlesource.com/c/sdk/+/152328 Change-Id: I7439750968595d25d7bbac0068ad64fcc891e176 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/155420 Commit-Queue: Clement Skau <cskau@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
parent
d878cfbf20
commit
6ca00d12c0
|
@ -157,6 +157,19 @@ Future awaitTimeout() async {
|
|||
await (throwAsync().timeout(Duration(seconds: 1)));
|
||||
}
|
||||
|
||||
// ----
|
||||
// Scenario: Future.wait:
|
||||
// ----
|
||||
|
||||
Future awaitWait() async {
|
||||
await Future.wait([
|
||||
throwAsync(),
|
||||
() async {
|
||||
await Future.value();
|
||||
}()
|
||||
]);
|
||||
}
|
||||
|
||||
// Helpers:
|
||||
|
||||
// We want lines that either start with a frame index or an async gap marker.
|
||||
|
@ -689,6 +702,48 @@ Future<void> doTestsCausal([String? debugInfoFilename]) async {
|
|||
r'^#6 _RawReceivePortImpl._handleMessage ',
|
||||
],
|
||||
debugInfoFilename);
|
||||
|
||||
final awaitWaitExpected = const <String>[
|
||||
r'^#0 throwAsync \(.*/utils.dart:21(:3)?\)$',
|
||||
r'^^<asynchronous suspension>$',
|
||||
r'^#1 awaitWait ',
|
||||
];
|
||||
await doTestAwait(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#2 doTestAwait ',
|
||||
r'^#3 doTestsCausal ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#4 main \(.+\)$',
|
||||
r'^#5 _startIsolate.<anonymous closure> ',
|
||||
r'^#6 _RawReceivePortImpl._handleMessage ',
|
||||
],
|
||||
debugInfoFilename);
|
||||
await doTestAwaitThen(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#2 doTestAwaitThen ',
|
||||
r'^#3 doTestsCausal ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#4 main \(.+\)$',
|
||||
r'^#5 _startIsolate.<anonymous closure> ',
|
||||
r'^#6 _RawReceivePortImpl._handleMessage ',
|
||||
],
|
||||
debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#2 doTestAwaitCatchError ',
|
||||
r'^#3 doTestsCausal ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#4 main \(.+\)$',
|
||||
r'^#5 _startIsolate.<anonymous closure> ',
|
||||
r'^#6 _RawReceivePortImpl._handleMessage ',
|
||||
],
|
||||
debugInfoFilename);
|
||||
}
|
||||
|
||||
// For: --no-causal-async-stacks --no-lazy-async-stacks
|
||||
|
@ -1027,6 +1082,25 @@ Future<void> doTestsNoCausalNoLazy([String? debugInfoFilename]) async {
|
|||
awaitTimeout, awaitTimeoutExpected + const <String>[], debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitTimeout, awaitTimeoutExpected + const <String>[], debugInfoFilename);
|
||||
|
||||
final awaitWaitExpected = const <String>[
|
||||
r'#0 throwAsync \(.*/utils.dart:21(:3)?\)$',
|
||||
r'^#1 _RootZone.runUnary ',
|
||||
r'^#2 _FutureListener.handleValue ',
|
||||
r'^#3 Future._propagateToListeners.handleValueCallback ',
|
||||
r'^#4 Future._propagateToListeners ',
|
||||
r'^#5 Future.(_addListener|_prependListeners).<anonymous closure> ',
|
||||
r'^#6 _microtaskLoop ',
|
||||
r'^#7 _startMicrotaskLoop ',
|
||||
r'^#8 _runPendingImmediateCallback ',
|
||||
r'^#9 _RawReceivePortImpl._handleMessage ',
|
||||
];
|
||||
await doTestAwait(
|
||||
awaitWait, awaitWaitExpected + const <String>[], debugInfoFilename);
|
||||
await doTestAwaitThen(
|
||||
awaitWait, awaitWaitExpected + const <String>[], debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitWait, awaitWaitExpected + const <String>[], debugInfoFilename);
|
||||
}
|
||||
|
||||
// For: --lazy-async-stacks
|
||||
|
@ -1319,4 +1393,35 @@ Future<void> doTestsLazy([String? debugInfoFilename]) async {
|
|||
debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitTimeout, awaitTimeoutExpected + const <String>[], debugInfoFilename);
|
||||
|
||||
final awaitWaitExpected = const <String>[
|
||||
r'^#0 throwAsync \(.*/utils.dart:21(:3)?\)$',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#1 Future.wait.<anonymous closure> \(dart:async/future.dart\)$',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#2 awaitWait ',
|
||||
r'^<asynchronous suspension>$',
|
||||
];
|
||||
await doTestAwait(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#3 doTestAwait ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#4 doTestsLazy ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#5 main ',
|
||||
r'^<asynchronous suspension>$',
|
||||
],
|
||||
debugInfoFilename);
|
||||
await doTestAwaitThen(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#3 doTestAwaitThen.<anonymous closure> ',
|
||||
r'^<asynchronous suspension>$',
|
||||
],
|
||||
debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitWait, awaitWaitExpected + const <String>[], debugInfoFilename);
|
||||
}
|
||||
|
|
|
@ -157,6 +157,19 @@ Future awaitTimeout() async {
|
|||
await (throwAsync().timeout(Duration(seconds: 1)));
|
||||
}
|
||||
|
||||
// ----
|
||||
// Scenario: Future.wait:
|
||||
// ----
|
||||
|
||||
Future awaitWait() async {
|
||||
await Future.wait([
|
||||
throwAsync(),
|
||||
() async {
|
||||
await Future.value();
|
||||
}()
|
||||
]);
|
||||
}
|
||||
|
||||
// Helpers:
|
||||
|
||||
// We want lines that either start with a frame index or an async gap marker.
|
||||
|
@ -689,6 +702,48 @@ Future<void> doTestsCausal([String debugInfoFilename]) async {
|
|||
r'^#6 _RawReceivePortImpl._handleMessage ',
|
||||
],
|
||||
debugInfoFilename);
|
||||
|
||||
final awaitWaitExpected = const <String>[
|
||||
r'^#0 throwAsync \(.*/utils.dart:21(:3)?\)$',
|
||||
r'^^<asynchronous suspension>$',
|
||||
r'^#1 awaitWait ',
|
||||
];
|
||||
await doTestAwait(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#2 doTestAwait ',
|
||||
r'^#3 doTestsCausal ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#4 main \(.+\)$',
|
||||
r'^#5 _startIsolate.<anonymous closure> ',
|
||||
r'^#6 _RawReceivePortImpl._handleMessage ',
|
||||
],
|
||||
debugInfoFilename);
|
||||
await doTestAwaitThen(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#2 doTestAwaitThen ',
|
||||
r'^#3 doTestsCausal ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#4 main \(.+\)$',
|
||||
r'^#5 _startIsolate.<anonymous closure> ',
|
||||
r'^#6 _RawReceivePortImpl._handleMessage ',
|
||||
],
|
||||
debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#2 doTestAwaitCatchError ',
|
||||
r'^#3 doTestsCausal ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#4 main \(.+\)$',
|
||||
r'^#5 _startIsolate.<anonymous closure> ',
|
||||
r'^#6 _RawReceivePortImpl._handleMessage ',
|
||||
],
|
||||
debugInfoFilename);
|
||||
}
|
||||
|
||||
// For: --no-causal-async-stacks --no-lazy-async-stacks
|
||||
|
@ -1027,6 +1082,25 @@ Future<void> doTestsNoCausalNoLazy([String debugInfoFilename]) async {
|
|||
awaitTimeout, awaitTimeoutExpected + const <String>[], debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitTimeout, awaitTimeoutExpected + const <String>[], debugInfoFilename);
|
||||
|
||||
final awaitWaitExpected = const <String>[
|
||||
r'#0 throwAsync \(.*/utils.dart:21(:3)?\)$',
|
||||
r'^#1 _RootZone.runUnary ',
|
||||
r'^#2 _FutureListener.handleValue ',
|
||||
r'^#3 Future._propagateToListeners.handleValueCallback ',
|
||||
r'^#4 Future._propagateToListeners ',
|
||||
r'^#5 Future.(_addListener|_prependListeners).<anonymous closure> ',
|
||||
r'^#6 _microtaskLoop ',
|
||||
r'^#7 _startMicrotaskLoop ',
|
||||
r'^#8 _runPendingImmediateCallback ',
|
||||
r'^#9 _RawReceivePortImpl._handleMessage ',
|
||||
];
|
||||
await doTestAwait(
|
||||
awaitWait, awaitWaitExpected + const <String>[], debugInfoFilename);
|
||||
await doTestAwaitThen(
|
||||
awaitWait, awaitWaitExpected + const <String>[], debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitWait, awaitWaitExpected + const <String>[], debugInfoFilename);
|
||||
}
|
||||
|
||||
// For: --lazy-async-stacks
|
||||
|
@ -1319,4 +1393,35 @@ Future<void> doTestsLazy([String debugInfoFilename]) async {
|
|||
debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitTimeout, awaitTimeoutExpected + const <String>[], debugInfoFilename);
|
||||
|
||||
final awaitWaitExpected = const <String>[
|
||||
r'^#0 throwAsync \(.*/utils.dart:21(:3)?\)$',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#1 Future.wait.<anonymous closure> \(dart:async/future.dart\)$',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#2 awaitWait ',
|
||||
r'^<asynchronous suspension>$',
|
||||
];
|
||||
await doTestAwait(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#3 doTestAwait ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#4 doTestsLazy ',
|
||||
r'^<asynchronous suspension>$',
|
||||
r'^#5 main ',
|
||||
r'^<asynchronous suspension>$',
|
||||
],
|
||||
debugInfoFilename);
|
||||
await doTestAwaitThen(
|
||||
awaitWait,
|
||||
awaitWaitExpected +
|
||||
const <String>[
|
||||
r'^#3 doTestAwaitThen.<anonymous closure> ',
|
||||
r'^<asynchronous suspension>$',
|
||||
],
|
||||
debugInfoFilename);
|
||||
await doTestAwaitCatchError(
|
||||
awaitWait, awaitWaitExpected + const <String>[], debugInfoFilename);
|
||||
}
|
||||
|
|
|
@ -409,7 +409,7 @@ ScopeBuildingResult* ScopeBuilder::BuildScopes() {
|
|||
}
|
||||
case FunctionLayout::kNoSuchMethodDispatcher:
|
||||
case FunctionLayout::kInvokeFieldDispatcher:
|
||||
case FunctionLayout::kFfiTrampoline:
|
||||
case FunctionLayout::kFfiTrampoline: {
|
||||
for (intptr_t i = 0; i < function.NumParameters(); ++i) {
|
||||
LocalVariable* variable = MakeVariable(
|
||||
TokenPosition::kNoSource, TokenPosition::kNoSource,
|
||||
|
@ -433,6 +433,7 @@ ScopeBuildingResult* ScopeBuilder::BuildScopes() {
|
|||
--depth_.catch_;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FunctionLayout::kSignatureFunction:
|
||||
case FunctionLayout::kIrregexpFunction:
|
||||
UNREACHABLE();
|
||||
|
@ -443,6 +444,7 @@ ScopeBuildingResult* ScopeBuilder::BuildScopes() {
|
|||
if (parsed_function_->function().MayHaveUncheckedEntryPoint()) {
|
||||
scope_->AddVariable(parsed_function_->EnsureEntryPointsTemp());
|
||||
}
|
||||
|
||||
parsed_function_->AllocateVariables();
|
||||
|
||||
return result_;
|
||||
|
@ -633,6 +635,13 @@ void ScopeBuilder::VisitFunctionNode() {
|
|||
LocalVariable* future = scope_->LookupVariable(Symbols::_future(), true);
|
||||
ASSERT(future != nullptr);
|
||||
future->set_is_chained_future();
|
||||
future->set_expected_context_index(Context::kFutureTimeoutFutureIndex);
|
||||
} else if (function.recognized_kind() == MethodRecognizer::kFutureWait &&
|
||||
depth_.function_ == 1) {
|
||||
LocalVariable* future = scope_->LookupVariable(Symbols::_future(), true);
|
||||
ASSERT(future != nullptr);
|
||||
future->set_is_chained_future();
|
||||
future->set_expected_context_index(Context::kFutureWaitFutureIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1313,7 +1322,8 @@ void ScopeBuilder::VisitVariableDeclaration() {
|
|||
variable->set_is_late();
|
||||
variable->set_late_init_offset(initializer_offset);
|
||||
}
|
||||
// Lift the two special async vars out of the function body scope, into the
|
||||
|
||||
// Lift the special async vars out of the function body scope, into the
|
||||
// outer function declaration scope.
|
||||
// This way we can allocate them in the outermost context at fixed indices,
|
||||
// allowing support for --lazy-async-stacks implementation to find awaiters.
|
||||
|
|
|
@ -178,6 +178,7 @@ namespace dart {
|
|||
V(::, reachabilityFence, ReachabilityFence, 0xad39d0a6) \
|
||||
V(_Utf8Decoder, _scan, Utf8DecoderScan, 0x78f44c3c) \
|
||||
V(_Future, timeout, FutureTimeout, 0x010f8ad4) \
|
||||
V(Future, wait, FutureWait, 0x486414a9) \
|
||||
|
||||
// List of intrinsics:
|
||||
// (class-name, function-name, intrinsification method, fingerprint).
|
||||
|
|
|
@ -6805,7 +6805,10 @@ class Context : public Object {
|
|||
static const intptr_t kAwaitJumpVarIndex = 0;
|
||||
static const intptr_t kAsyncCompleterIndex = 1;
|
||||
static const intptr_t kControllerIndex = 1;
|
||||
static const intptr_t kChainedFutureIndex = 2;
|
||||
// Expected context index of chained futures in recognized async functions.
|
||||
// These are used to unwind async stacks.
|
||||
static const intptr_t kFutureTimeoutFutureIndex = 2;
|
||||
static const intptr_t kFutureWaitFutureIndex = 2;
|
||||
|
||||
static intptr_t variable_offset(intptr_t context_index) {
|
||||
return OFFSET_OF_RETURNED_VALUE(ContextLayout, data) +
|
||||
|
|
|
@ -249,7 +249,8 @@ VariableIndex LocalScope::AllocateVariables(VariableIndex first_parameter_index,
|
|||
if (chained_future != nullptr) {
|
||||
AllocateContextVariable(chained_future, &context_owner);
|
||||
*found_captured_variables = true;
|
||||
ASSERT(chained_future->index().value() == Context::kChainedFutureIndex);
|
||||
ASSERT(chained_future->index().value() ==
|
||||
chained_future->expected_context_index());
|
||||
}
|
||||
|
||||
while (pos < num_parameters) {
|
||||
|
@ -279,7 +280,7 @@ VariableIndex LocalScope::AllocateVariables(VariableIndex first_parameter_index,
|
|||
LocalVariable* variable = VariableAt(pos);
|
||||
if (variable->owner() == this) {
|
||||
if (variable->is_captured()) {
|
||||
// Skip the two variables already pre-allocated above.
|
||||
// Skip the variables already pre-allocated above.
|
||||
if (variable != await_jump_var && variable != async_completer &&
|
||||
variable != controller && variable != chained_future) {
|
||||
AllocateContextVariable(variable, &context_owner);
|
||||
|
|
|
@ -94,6 +94,7 @@ class LocalVariable : public ZoneAllocated {
|
|||
is_explicit_covariant_parameter_(false),
|
||||
is_late_(false),
|
||||
is_chained_future_(false),
|
||||
expected_context_index_(-1),
|
||||
late_init_offset_(0),
|
||||
type_check_mode_(kDoTypeCheck),
|
||||
index_() {
|
||||
|
@ -135,6 +136,11 @@ class LocalVariable : public ZoneAllocated {
|
|||
bool is_chained_future() const { return is_chained_future_; }
|
||||
void set_is_chained_future() { is_chained_future_ = true; }
|
||||
|
||||
intptr_t expected_context_index() const { return expected_context_index_; }
|
||||
void set_expected_context_index(int index) {
|
||||
expected_context_index_ = index;
|
||||
}
|
||||
|
||||
intptr_t late_init_offset() const { return late_init_offset_; }
|
||||
void set_late_init_offset(intptr_t late_init_offset) {
|
||||
late_init_offset_ = late_init_offset;
|
||||
|
@ -225,6 +231,7 @@ class LocalVariable : public ZoneAllocated {
|
|||
bool is_explicit_covariant_parameter_;
|
||||
bool is_late_;
|
||||
bool is_chained_future_;
|
||||
intptr_t expected_context_index_;
|
||||
intptr_t late_init_offset_;
|
||||
TypeCheckMode type_check_mode_;
|
||||
VariableIndex index_;
|
||||
|
|
|
@ -131,8 +131,7 @@ class CallerClosureFinder {
|
|||
var_data_field(Field::Handle(zone)),
|
||||
state_field(Field::Handle(zone)),
|
||||
on_data_field(Field::Handle(zone)),
|
||||
state_data_field(Field::Handle(zone)),
|
||||
future_timeout_method_(Function::Handle(zone)) {
|
||||
state_data_field(Field::Handle(zone)) {
|
||||
const auto& async_lib = Library::Handle(zone, Library::AsyncLibrary());
|
||||
// Look up classes:
|
||||
// - async:
|
||||
|
@ -197,11 +196,6 @@ class CallerClosureFinder {
|
|||
state_data_field =
|
||||
stream_iterator_class.LookupFieldAllowPrivate(Symbols::_stateData());
|
||||
ASSERT(!state_data_field.IsNull());
|
||||
|
||||
// Functions:
|
||||
future_timeout_method_ =
|
||||
future_impl_class.LookupFunction(Symbols::timeout());
|
||||
ASSERT(!future_timeout_method_.IsNull());
|
||||
}
|
||||
|
||||
ClosurePtr GetCallerInFutureImpl(const Object& future_) {
|
||||
|
@ -296,7 +290,14 @@ class CallerClosureFinder {
|
|||
parent_function_ = receiver_function_.parent_function();
|
||||
if (parent_function_.recognized_kind() ==
|
||||
MethodRecognizer::kFutureTimeout) {
|
||||
context_entry_ = receiver_context_.At(Context::kChainedFutureIndex);
|
||||
context_entry_ =
|
||||
receiver_context_.At(Context::kFutureTimeoutFutureIndex);
|
||||
return GetCallerInFutureImpl(context_entry_);
|
||||
} else if (parent_function_.recognized_kind() ==
|
||||
MethodRecognizer::kFutureWait) {
|
||||
receiver_context_ = receiver_context_.parent();
|
||||
ASSERT(!receiver_context_.IsNull());
|
||||
context_entry_ = receiver_context_.At(Context::kFutureWaitFutureIndex);
|
||||
return GetCallerInFutureImpl(context_entry_);
|
||||
}
|
||||
}
|
||||
|
@ -363,8 +364,6 @@ class CallerClosureFinder {
|
|||
Field& state_field;
|
||||
Field& on_data_field;
|
||||
Field& state_data_field;
|
||||
|
||||
Function& future_timeout_method_;
|
||||
};
|
||||
|
||||
void StackTraceUtils::CollectFramesLazy(
|
||||
|
|
|
@ -360,9 +360,12 @@ abstract class Future<T> {
|
|||
* The call to [cleanUp] should not throw. If it does, the error will be an
|
||||
* uncaught asynchronous error.
|
||||
*/
|
||||
@pragma("vm:entry-point")
|
||||
static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
|
||||
{bool eagerError = false, void cleanUp(T successValue)?}) {
|
||||
final _Future<List<T>> result = new _Future<List<T>>();
|
||||
// This is a VM recognised method, and the _future variable is deliberately
|
||||
// allocated in a specific slot in the closure context for stack unwinding.
|
||||
final _Future<List<T>> _future = _Future<List<T>>();
|
||||
List<T?>? values; // Collects the values. Set to null on error.
|
||||
int remaining = 0; // How many futures are we waiting for.
|
||||
late Object error; // The first error from a future.
|
||||
|
@ -386,13 +389,13 @@ abstract class Future<T> {
|
|||
}
|
||||
values = null;
|
||||
if (remaining == 0 || eagerError) {
|
||||
result._completeError(theError, theStackTrace);
|
||||
_future._completeError(theError, theStackTrace);
|
||||
} else {
|
||||
error = theError;
|
||||
stackTrace = theStackTrace;
|
||||
}
|
||||
} else if (remaining == 0 && !eagerError) {
|
||||
result._completeError(error, stackTrace);
|
||||
_future._completeError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,7 +410,7 @@ abstract class Future<T> {
|
|||
if (valueList != null) {
|
||||
valueList[pos] = value;
|
||||
if (remaining == 0) {
|
||||
result._completeWithValue(List<T>.from(valueList));
|
||||
_future._completeWithValue(List<T>.from(valueList));
|
||||
}
|
||||
} else {
|
||||
if (cleanUp != null && value != null) {
|
||||
|
@ -419,7 +422,7 @@ abstract class Future<T> {
|
|||
if (remaining == 0 && !eagerError) {
|
||||
// If eagerError is false, and valueList is null, then
|
||||
// error and stackTrace have been set in handleError above.
|
||||
result._completeError(error, stackTrace);
|
||||
_future._completeError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
}, onError: handleError);
|
||||
|
@ -439,7 +442,7 @@ abstract class Future<T> {
|
|||
// gracefully.
|
||||
if (remaining == 0 || eagerError) {
|
||||
// Throw a new Future.error.
|
||||
// Don't just call `result._completeError` since that would propagate
|
||||
// Don't just call `_future._completeError` since that would propagate
|
||||
// the error too eagerly, not giving the callers time to install
|
||||
// error handlers.
|
||||
// Also, don't use `_asyncCompleteError` since that one doesn't give
|
||||
|
@ -453,7 +456,7 @@ abstract class Future<T> {
|
|||
stackTrace = st;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return _future;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue