diff --git a/pkg/dart2wasm/lib/class_info.dart b/pkg/dart2wasm/lib/class_info.dart index bcba05daab7..8480712e48c 100644 --- a/pkg/dart2wasm/lib/class_info.dart +++ b/pkg/dart2wasm/lib/class_info.dart @@ -718,6 +718,9 @@ class ClassIdNumbering { firstClassId + concreteClassCount - 1, firstClassId + concreteClassCount + abstractClassCount - 1); } + + Range getConcreteSubclassRange(Class klass) => + _concreteSubclassIdRange[klass]!; } // A range of class ids, both ends inclusive. diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart index 6d91ef7ba3d..aeb05a95b26 100644 --- a/pkg/dart2wasm/lib/intrinsics.dart +++ b/pkg/dart2wasm/lib/intrinsics.dart @@ -1154,6 +1154,17 @@ class Intrinsifier { codeGen.wrap(value, w.RefType.extern(nullable: true)); b.ref_is_null(); return w.NumType.i32; + case "isSubClassOf": + final baseClass = + (node.arguments.types.single as InterfaceType).classNode; + final range = + translator.classIdNumbering.getConcreteSubclassRange(baseClass); + + final object = node.arguments.positional.single; + codeGen.wrap(object, w.RefType.any(nullable: false)); + b.struct_get(translator.topInfo.struct, FieldIndex.classId); + b.emitClassIdRangeCheck([range]); + return w.NumType.i32; } } diff --git a/pkg/dart2wasm/lib/transformers.dart b/pkg/dart2wasm/lib/transformers.dart index ef049a012c2..69c896dc808 100644 --- a/pkg/dart2wasm/lib/transformers.dart +++ b/pkg/dart2wasm/lib/transformers.dart @@ -53,6 +53,9 @@ class _WasmTransformer extends Transformer { final Procedure _streamControllerSetOnListen; final Procedure _streamControllerSetOnResume; + final Procedure _trySetStackTraceForwarder; + final Procedure _trySetStackTrace; + final List<_AsyncStarFrame> _asyncStarFrames = []; bool _enclosingIsAsyncStar = false; @@ -104,6 +107,10 @@ class _WasmTransformer extends Transformer { .getProcedure('dart:async', 'StreamController', 'set:onListen'), _streamControllerSetOnResume = coreTypes.index .getProcedure('dart:async', 'StreamController', 'set:onResume'), + _trySetStackTraceForwarder = coreTypes.index + .getTopLevelProcedure('dart:async', '_trySetStackTrace'), + _trySetStackTrace = coreTypes.index + .getProcedure('dart:core', 'Error', '_trySetStackTrace'), _listFactorySpecializer = ListFactorySpecializer(coreTypes), _pushPopWasmArrayTransformer = PushPopWasmArrayTransformer(coreTypes); @@ -716,6 +723,12 @@ class _WasmTransformer extends Transformer { @override TreeNode visitStaticInvocation(StaticInvocation node) { node.transformChildren(this); + + // Forward calls in `dart:async` to private `dart:core` method. + if (node.target == _trySetStackTraceForwarder) { + node.target = _trySetStackTrace; + } + return _pushPopWasmArrayTransformer.transformStaticInvocation( _listFactorySpecializer.transformStaticInvocation(node)); } diff --git a/pkg/front_end/test/macros/application/data/tests/crash.dart.expect b/pkg/front_end/test/macros/application/data/tests/crash.dart.expect index a18d56f282d..0d84c8efe58 100644 --- a/pkg/front_end/test/macros/application/data/tests/crash.dart.expect +++ b/pkg/front_end/test/macros/application/data/tests/crash.dart.expect @@ -10,24 +10,24 @@ library; // #1 executeTypesMacro (package:_macros/src/executor/execute_macro.dart:39:21) // #2 MacroExpansionClient._executeTypesPhase. (package:_macros/src/executor/client.dart:219:17) // #3 runPhase. (package:_macros/src/executor/client.dart:385:30) -// #4 _rootRun (dart:async/zone.dart:1399:13) -// #5 _CustomZone.run (dart:async/zone.dart:1301:19) -// #6 _CustomZone.runGuarded (dart:async/zone.dart:1209:7) +// #4 _rootRun (dart:async/zone.dart:1414:13) +// #5 _CustomZone.run (dart:async/zone.dart:1317:19) +// #6 _CustomZone.runGuarded (dart:async/zone.dart:1225:7) // #7 runPhase (package:_macros/src/executor/client.dart:385:8) // #8 MacroExpansionClient._executeTypesPhase (package:_macros/src/executor/client.dart:218:43) // #9 MacroExpansionClient._handleMessage. (package:_macros/src/executor/client.dart:145:18) -// #10 _rootRun (dart:async/zone.dart:1399:13) -// #11 _CustomZone.run (dart:async/zone.dart:1301:19) +// #10 _rootRun (dart:async/zone.dart:1414:13) +// #11 _CustomZone.run (dart:async/zone.dart:1317:19) // #12 withRemoteInstanceZone (package:_macros/src/executor/remote_instance.dart:172:15) // #13 MacroExpansionClient._handleMessage (package:_macros/src/executor/client.dart:118:11) // #14 new MacroExpansionClient._. (package:_macros/src/executor/client.dart:37:39) -// #15 _rootRunUnary (dart:async/zone.dart:1415:13) -// #16 _CustomZone.runUnary (dart:async/zone.dart:1308:19) -// #17 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1217:7) -// #18 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:365:11) +// #15 _rootRunUnary (dart:async/zone.dart:1430:13) +// #16 _CustomZone.runUnary (dart:async/zone.dart:1324:19) +// #17 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1233:7) +// #18 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:366:11) // #19 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:297:7) -// #20 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:784:19) -// #21 _StreamController._add (dart:async/stream_controller.dart:658:7) +// #20 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:777:19) +// #21 _StreamController._add (dart:async/stream_controller.dart:651:7) // #22 _StreamController.add (dart:async/stream_controller.dart:606:5) // #23 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12) // @@ -42,24 +42,24 @@ library; // #1 executeDeclarationsMacro (package:_macros/src/executor/execute_macro.dart:107:21) // #2 MacroExpansionClient._executeDeclarationsPhase. (package:_macros/src/executor/client.dart:248:11) // #3 runPhase. (package:_macros/src/executor/client.dart:385:30) -// #4 _rootRun (dart:async/zone.dart:1399:13) -// #5 _CustomZone.run (dart:async/zone.dart:1301:19) -// #6 _CustomZone.runGuarded (dart:async/zone.dart:1209:7) +// #4 _rootRun (dart:async/zone.dart:1414:13) +// #5 _CustomZone.run (dart:async/zone.dart:1317:19) +// #6 _CustomZone.runGuarded (dart:async/zone.dart:1225:7) // #7 runPhase (package:_macros/src/executor/client.dart:385:8) // #8 MacroExpansionClient._executeDeclarationsPhase (package:_macros/src/executor/client.dart:247:43) // #9 MacroExpansionClient._handleMessage. (package:_macros/src/executor/client.dart:135:18) -// #10 _rootRun (dart:async/zone.dart:1399:13) -// #11 _CustomZone.run (dart:async/zone.dart:1301:19) +// #10 _rootRun (dart:async/zone.dart:1414:13) +// #11 _CustomZone.run (dart:async/zone.dart:1317:19) // #12 withRemoteInstanceZone (package:_macros/src/executor/remote_instance.dart:172:15) // #13 MacroExpansionClient._handleMessage (package:_macros/src/executor/client.dart:118:11) // #14 new MacroExpansionClient._. (package:_macros/src/executor/client.dart:37:39) -// #15 _rootRunUnary (dart:async/zone.dart:1415:13) -// #16 _CustomZone.runUnary (dart:async/zone.dart:1308:19) -// #17 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1217:7) -// #18 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:365:11) +// #15 _rootRunUnary (dart:async/zone.dart:1430:13) +// #16 _CustomZone.runUnary (dart:async/zone.dart:1324:19) +// #17 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1233:7) +// #18 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:366:11) // #19 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:297:7) -// #20 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:784:19) -// #21 _StreamController._add (dart:async/stream_controller.dart:658:7) +// #20 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:777:19) +// #21 _StreamController._add (dart:async/stream_controller.dart:651:7) // #22 _StreamController.add (dart:async/stream_controller.dart:606:5) // #23 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12) // @@ -74,24 +74,24 @@ library; // #1 executeDefinitionMacro (package:_macros/src/executor/execute_macro.dart:167:21) // #2 MacroExpansionClient._executeDefinitionsPhase. (package:_macros/src/executor/client.dart:276:17) // #3 runPhase. (package:_macros/src/executor/client.dart:385:30) -// #4 _rootRun (dart:async/zone.dart:1399:13) -// #5 _CustomZone.run (dart:async/zone.dart:1301:19) -// #6 _CustomZone.runGuarded (dart:async/zone.dart:1209:7) +// #4 _rootRun (dart:async/zone.dart:1414:13) +// #5 _CustomZone.run (dart:async/zone.dart:1317:19) +// #6 _CustomZone.runGuarded (dart:async/zone.dart:1225:7) // #7 runPhase (package:_macros/src/executor/client.dart:385:8) // #8 MacroExpansionClient._executeDefinitionsPhase (package:_macros/src/executor/client.dart:275:43) // #9 MacroExpansionClient._handleMessage. (package:_macros/src/executor/client.dart:140:18) -// #10 _rootRun (dart:async/zone.dart:1399:13) -// #11 _CustomZone.run (dart:async/zone.dart:1301:19) +// #10 _rootRun (dart:async/zone.dart:1414:13) +// #11 _CustomZone.run (dart:async/zone.dart:1317:19) // #12 withRemoteInstanceZone (package:_macros/src/executor/remote_instance.dart:172:15) // #13 MacroExpansionClient._handleMessage (package:_macros/src/executor/client.dart:118:11) // #14 new MacroExpansionClient._. (package:_macros/src/executor/client.dart:37:39) -// #15 _rootRunUnary (dart:async/zone.dart:1415:13) -// #16 _CustomZone.runUnary (dart:async/zone.dart:1308:19) -// #17 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1217:7) -// #18 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:365:11) +// #15 _rootRunUnary (dart:async/zone.dart:1430:13) +// #16 _CustomZone.runUnary (dart:async/zone.dart:1324:19) +// #17 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1233:7) +// #18 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:366:11) // #19 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:297:7) -// #20 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:784:19) -// #21 _StreamController._add (dart:async/stream_controller.dart:658:7) +// #20 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:777:19) +// #21 _StreamController._add (dart:async/stream_controller.dart:651:7) // #22 _StreamController.add (dart:async/stream_controller.dart:606:5) // #23 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12) // diff --git a/runtime/lib/errors.cc b/runtime/lib/errors.cc index bf194e5c60a..d770c8753a7 100644 --- a/runtime/lib/errors.cc +++ b/runtime/lib/errors.cc @@ -182,4 +182,12 @@ DEFINE_NATIVE_ENTRY(Error_throwWithStackTrace, 0, 2) { return Object::null(); } +// Sets stack trace on object, if it's an `Error` with no `Error.stackTrace`. +DEFINE_NATIVE_ENTRY(Error_trySetStackTrace, 0, 2) { + GET_NON_NULL_NATIVE_ARGUMENT(Instance, error, arguments->NativeArgAt(0)); + GET_NON_NULL_NATIVE_ARGUMENT(Instance, stacktrace, arguments->NativeArgAt(1)); + Exceptions::TrySetStackTrace(zone, error, stacktrace); + return Object::null(); +} + } // namespace dart diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h index b4150b46f6d..ad015401566 100644 --- a/runtime/vm/bootstrap_natives.h +++ b/runtime/vm/bootstrap_natives.h @@ -140,6 +140,7 @@ namespace dart { V(AssertionError_throwNew, 3) \ V(AssertionError_throwNewSource, 5) \ V(Error_throwWithStackTrace, 2) \ + V(Error_trySetStackTrace, 2) \ V(StackTrace_current, 0) \ V(TypeError_throwNew, 4) \ V(Stopwatch_now, 0) \ diff --git a/runtime/vm/exceptions.cc b/runtime/vm/exceptions.cc index 69d963993b3..5682181a1ea 100644 --- a/runtime/vm/exceptions.cc +++ b/runtime/vm/exceptions.cc @@ -822,12 +822,7 @@ static void ThrowExceptionHelper(Thread* thread, // If this is not a rethrow, it's a "throw with stacktrace". // Set an Error object's stackTrace field if needed. if (!is_rethrow) { - const Field& stacktrace_field = - Field::Handle(zone, LookupStackTraceField(exception)); - if (!stacktrace_field.IsNull() && - (exception.GetField(stacktrace_field) == Object::null())) { - exception.SetField(stacktrace_field, stacktrace); - } + Exceptions::TrySetStackTrace(zone, exception, stacktrace); } } else { // Get stacktrace field of class Error to determine whether we have a @@ -1023,6 +1018,17 @@ void Exceptions::ThrowWithStackTrace(Thread* thread, /*bypass_debugger=*/false); } +void Exceptions::TrySetStackTrace(Zone* zone, + const Instance& error, + const Instance& stacktrace) { + const Field& stacktrace_field = + Field::Handle(zone, dart::LookupStackTraceField(error)); + if (!stacktrace_field.IsNull() && + (error.GetField(stacktrace_field) == Object::null())) { + error.SetField(stacktrace_field, stacktrace); + } +} + void Exceptions::PropagateError(const Error& error) { ASSERT(!error.IsNull()); Thread* thread = Thread::Current(); diff --git a/runtime/vm/exceptions.h b/runtime/vm/exceptions.h index fe94de8c53b..abb14316cdb 100644 --- a/runtime/vm/exceptions.h +++ b/runtime/vm/exceptions.h @@ -41,6 +41,11 @@ class Exceptions : AllStatic { // Propagate an error to the entry frame, skipping over Dart frames. DART_NORETURN static void PropagateToEntry(const Error& error); + // Sets `Error._stackTrace` if possible. + static void TrySetStackTrace(Zone* zone, + const Instance& exception, + const Instance& stacktrace); + // Helpers to create and throw errors. static StackTracePtr CurrentStackTrace(); static ScriptPtr GetCallerScript(DartFrameIterator* iterator); diff --git a/sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart index 309d79713e9..3e919568c1d 100644 --- a/sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart +++ b/sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart @@ -12,6 +12,13 @@ import 'dart:_foreign_helper' show JS, JS_RAW_EXCEPTION, JSExportName, RAW_DART_FUNCTION_REF; import 'dart:_runtime' as dart; +@patch +void _trySetStackTrace(Object error, StackTrace stackTrace) { + if (error is Error) { + dart.trySetStackTrace(error, stackTrace); + } +} + /// This function adapts ES6 generators to implement Dart's async/await. /// /// It's designed to interact with Dart's Future and follow Dart async/await diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart index da32ef20346..eb2f5311f90 100644 --- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart +++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart @@ -208,6 +208,15 @@ StackTrace? stackTraceForError(Error error) { return stackTrace(jsError); } +/// Sets stack trace on error, if possible. +void trySetStackTrace(Error error, StackTrace stackTrace) { + var jsError = JS('', '#[#]', error, _jsError); + if (jsError == null) { + // Link Dart stack trace to error object. + JS('', 'new #(#, #)', RethrownDartError, error, stackTrace); + } +} + /// Implements `rethrow` of [error], allowing rethrow in an expression context. /// /// Note: [error] must be the raw JS error caught in the JS catch, not the diff --git a/sdk/lib/_internal/js_runtime/lib/async_patch.dart b/sdk/lib/_internal/js_runtime/lib/async_patch.dart index 129a23d8021..3bca5bf4e54 100644 --- a/sdk/lib/_internal/js_runtime/lib/async_patch.dart +++ b/sdk/lib/_internal/js_runtime/lib/async_patch.dart @@ -10,6 +10,7 @@ import 'dart:_js_helper' ExceptionAndStackTrace, convertDartClosureToJS, getTraceFromException, + Primitives, requiresPreamble, wrapException, unwrapException; @@ -19,6 +20,13 @@ import 'dart:_foreign_helper' import 'dart:_async_status_codes' as async_status_codes; +@patch +void _trySetStackTrace(Object error, StackTrace stackTrace) { + if (error is Error) { + Primitives.trySetStackTrace(error, stackTrace); + } +} + @patch class _AsyncRun { @patch diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart index 37c50335a29..ab5d96fdc8a 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart @@ -1098,6 +1098,15 @@ class Primitives { if (jsError == null) return null; return getTraceFromException(jsError); } + + static void trySetStackTrace(Error error, StackTrace stackTrace) { + var jsError = JS('', r'#.$thrownJsError', error); + if (jsError == null) { + jsError = wrapException(error); + JS('', r'#.$thrownJsError = #', error, jsError); + JS('void', '#.stack = #', jsError, stackTrace.toString()); + } + } } /// Called by generated code to throw an illegal-argument exception, @@ -1193,7 +1202,7 @@ String checkString(value) { return value; } -/// Wrap the given Dart object and record a stack trace. +/// Wrap the given Dart object as a JS `Error` that can carry a stack trace. /// /// The code in [unwrapException] deals with getting the original Dart /// object out of the wrapper again. diff --git a/sdk/lib/_internal/vm/lib/async_patch.dart b/sdk/lib/_internal/vm/lib/async_patch.dart index 21f98ce4d65..f74d975fd9b 100644 --- a/sdk/lib/_internal/vm/lib/async_patch.dart +++ b/sdk/lib/_internal/vm/lib/async_patch.dart @@ -17,6 +17,10 @@ part "timer_patch.dart"; @pragma("vm:external-name", "DartAsync_fatal") external _fatal(msg); +@patch +@pragma("vm:external-name", "Error_trySetStackTrace") +external void _trySetStackTrace(Object error, StackTrace stackTrace); + // This function is used when lowering `await for` statements. void _asyncStarMoveNextHelper(var stream) { if (stream is! _StreamImpl) { diff --git a/sdk/lib/_internal/wasm/lib/async_patch.dart b/sdk/lib/_internal/wasm/lib/async_patch.dart index e4d2ed3507c..1f8eb87a3a9 100644 --- a/sdk/lib/_internal/wasm/lib/async_patch.dart +++ b/sdk/lib/_internal/wasm/lib/async_patch.dart @@ -6,14 +6,19 @@ import 'dart:_wasm'; part 'timer_patch.dart'; +// Modular kernel transformer will make calls to this method be re-directed to +// call dart:core:Error._trySetStackTrace instead. +@patch +external void _trySetStackTrace(Object error, StackTrace stackTrace); + typedef _AsyncResumeFun = WasmFunction< void Function( _AsyncSuspendState, // Value of the last `await` Object?, - // If the last `await` throwed an error, the error value + // If the last `await` threw an error, the error value Object?, - // If the last `await` throwed an error, the stack trace + // If the last `await` threw an error, the stack trace StackTrace?)>; @pragma("wasm:entry-point") diff --git a/sdk/lib/_internal/wasm/lib/errors_patch.dart b/sdk/lib/_internal/wasm/lib/errors_patch.dart index 551047daa5e..6a0f89cd9f4 100644 --- a/sdk/lib/_internal/wasm/lib/errors_patch.dart +++ b/sdk/lib/_internal/wasm/lib/errors_patch.dart @@ -23,6 +23,15 @@ class Error { @pragma("wasm:entry-point") StackTrace? _stackTrace; + + static void _trySetStackTrace(Object object, StackTrace stackTrace) { + // Guard against implementors of [Error] that do not have the stack trace + // field by ensuring the error object is a direct/indirect subclass. + if (isSubClassOf(object)) { + final error = unsafeCast(object); + error._stackTrace ??= stackTrace; + } + } } class _Error extends Error { diff --git a/sdk/lib/_wasm/wasm_types.dart b/sdk/lib/_wasm/wasm_types.dart index 3d20cc67052..47050a8f82d 100644 --- a/sdk/lib/_wasm/wasm_types.dart +++ b/sdk/lib/_wasm/wasm_types.dart @@ -395,3 +395,9 @@ extension WasmExternRefToJSAny on WasmExternRef { // on JS interop types are assumed to be JS interop functions, not methods that // are patched in patch files. So instead we just use a plain function here. external WasmExternRef? externRefForJSAny(JSAny object); + +// Tests whether the given object's class is a subclass of T. +// +// NOTICE: If the object's class is a subtype of T but not a subclass this will +// return `false`. +external bool isSubClassOf(Object object); diff --git a/sdk/lib/async/async_error.dart b/sdk/lib/async/async_error.dart index ed415479cee..179e08c3c3b 100644 --- a/sdk/lib/async/async_error.dart +++ b/sdk/lib/async/async_error.dart @@ -13,9 +13,11 @@ final class AsyncError implements Error { final StackTrace stackTrace; AsyncError(Object error, StackTrace? stackTrace) - : error = checkNotNullable(error, "error"), + : error = error, stackTrace = stackTrace ?? defaultStackTrace(error); + AsyncError._(this.error, this.stackTrace); + /// A default stack trace for an error. /// /// If [error] is an [Error] and it has an [Error.stackTrace], diff --git a/sdk/lib/async/broadcast_stream_controller.dart b/sdk/lib/async/broadcast_stream_controller.dart index 598ec44a0d1..1e686ad22bc 100644 --- a/sdk/lib/async/broadcast_stream_controller.dart +++ b/sdk/lib/async/broadcast_stream_controller.dart @@ -245,16 +245,9 @@ abstract class _BroadcastStreamController } void addError(Object error, [StackTrace? stackTrace]) { - checkNotNullable(error, "error"); if (!_mayAddEvent) throw _addEventError(); - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); - if (replacement != null) { - error = replacement.error; - stackTrace = replacement.stackTrace; - } else { - stackTrace ??= AsyncError.defaultStackTrace(error); - } - _sendError(error, stackTrace); + AsyncError(:error, :stackTrace) = _interceptUserError(error, stackTrace); + _addError(error, stackTrace); } Future close() { @@ -476,14 +469,13 @@ class _AsBroadcastStreamController extends _SyncBroadcastStreamController } void addError(Object error, [StackTrace? stackTrace]) { - checkNotNullable(error, "error"); - stackTrace ??= AsyncError.defaultStackTrace(error); + if (!_mayAddEvent) throw _addEventError(); + AsyncError(:error, :stackTrace) = _interceptUserError(error, stackTrace); if (!isClosed && _isFiring) { _addPendingEvent(new _DelayedError(error, stackTrace)); return; } - if (!_mayAddEvent) throw _addEventError(); - _sendError(error, stackTrace); + _addError(error, stackTrace); _flushPending(); } diff --git a/sdk/lib/async/future.dart b/sdk/lib/async/future.dart index a6abe4940ed..e9dfc998202 100644 --- a/sdk/lib/async/future.dart +++ b/sdk/lib/async/future.dart @@ -313,12 +313,7 @@ abstract interface class Future { result = computation(); } catch (error, stackTrace) { var future = new _Future(); - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); - if (replacement != null) { - future._asyncCompleteError(replacement.error, replacement.stackTrace); - } else { - future._asyncCompleteError(error, stackTrace); - } + _asyncCompleteWithErrorCallback(future, error, stackTrace); return future; } return result is Future ? result : _Future.value(result); @@ -373,16 +368,7 @@ abstract interface class Future { /// final error = await getFuture(); // Throws. /// ``` factory Future.error(Object error, [StackTrace? stackTrace]) { - // TODO(40614): Remove once non-nullability is sound. - checkNotNullable(error, "error"); - if (!identical(Zone.current, _rootZone)) { - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); - if (replacement != null) { - error = replacement.error; - stackTrace = replacement.stackTrace; - } - } - stackTrace ??= AsyncError.defaultStackTrace(error); + AsyncError(:error, :stackTrace) = _interceptUserError(error, stackTrace); return new _Future.immediateError(error, stackTrace); } @@ -1335,27 +1321,26 @@ abstract interface class Completer { // Helper function completing a _Future with error, but checking the zone // for error replacement and missing stack trace first. +// Only used for errors that are *caught*. +// A user provided error object should use `_interceptUserError` which +// also sets `Error.stackTrace`. void _completeWithErrorCallback( - _Future result, Object error, StackTrace? stackTrace) { - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); + _Future result, Object error, StackTrace stackTrace) { + var replacement = _interceptError(error, stackTrace); if (replacement != null) { error = replacement.error; stackTrace = replacement.stackTrace; - } else { - stackTrace ??= AsyncError.defaultStackTrace(error); } result._completeError(error, stackTrace); } // Like [_completeWithErrorCallback] but completes asynchronously. void _asyncCompleteWithErrorCallback( - _Future result, Object error, StackTrace? stackTrace) { - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); + _Future result, Object error, StackTrace stackTrace) { + var replacement = _interceptError(error, stackTrace); if (replacement != null) { error = replacement.error; stackTrace = replacement.stackTrace; - } else { - stackTrace ??= AsyncError.defaultStackTrace(error); } result._asyncCompleteError(error, stackTrace); } diff --git a/sdk/lib/async/future_impl.dart b/sdk/lib/async/future_impl.dart index 85770f1621f..1f6fa46c038 100644 --- a/sdk/lib/async/future_impl.dart +++ b/sdk/lib/async/future_impl.dart @@ -4,6 +4,55 @@ part of dart.async; +/// Sets [stackTrace] as [Error.stackTrace] on [error], if that is an [Error]. +/// +/// If [error] is not an instance of a class which extends [Error], +/// or if it already has an [Error.stackTrace] set, nothing happens. +@pragma("wasm:entry-point") +external void _trySetStackTrace(Object error, StackTrace stackTrace); + +/// Calls the [Zone.errorCallback] of the current zone with the error and stack. +/// +/// If a non-`null` result is returned, it tries to set the stack trace +/// on the returned error to the returned stack trace. +AsyncError? _interceptError(Object error, StackTrace? stackTrace) { + var zone = Zone._current; + if (identical(zone, _rootZone)) return null; + var replacement = zone.errorCallback(error, stackTrace); + if (replacement == null) return null; + _trySetStackTrace(replacement.error, replacement.stackTrace); + return replacement; +} + +/// Used for user-provided error and stack trace that are to become async +/// errors. +/// +/// Allows `Zone.current.errorCallback` to intercept and modify the error, +/// and sets the stack trace on the error as if it was thrown. +AsyncError _interceptUserError(Object error, StackTrace? stackTrace) { + var zone = Zone.current; + if (!identical(zone, _rootZone)) { + var replacement = _interceptError(error, stackTrace); + if (replacement != null) return replacement; + } + if (stackTrace == null) { + // Inlines `AsyncError.defaultStackTrace`, to reuse the `is Error` check + // result to also not do `_trySetStackTrace` on known non-`Error`s. + if (error is Error) { + stackTrace = error.stackTrace; + if (stackTrace == null) { + stackTrace = StackTrace.empty; + _trySetStackTrace(error, stackTrace); + } + } else { + stackTrace = StackTrace.empty; + } + } else { + _trySetStackTrace(error, stackTrace); // Maybe it's an Error. + } + return AsyncError._(error, stackTrace); +} + abstract class _Completer implements Completer { @pragma("wasm:entry-point") @pragma("vm:entry-point") @@ -14,16 +63,8 @@ abstract class _Completer implements Completer { @pragma("wasm:entry-point") void completeError(Object error, [StackTrace? stackTrace]) { - // TODO(40614): Remove once non-nullability is sound. - checkNotNullable(error, "error"); if (!future._mayComplete) throw new StateError("Future already completed"); - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); - if (replacement != null) { - error = replacement.error; - stackTrace = replacement.stackTrace; - } else { - stackTrace ??= AsyncError.defaultStackTrace(error); - } + AsyncError(:error, :stackTrace) = _interceptUserError(error, stackTrace); _completeError(error, stackTrace); } diff --git a/sdk/lib/async/stream.dart b/sdk/lib/async/stream.dart index b36f68f5a0e..e103fa507ed 100644 --- a/sdk/lib/async/stream.dart +++ b/sdk/lib/async/stream.dart @@ -212,10 +212,9 @@ abstract mixin class Stream { /// stack trace as well. @Since("2.5") factory Stream.error(Object error, [StackTrace? stackTrace]) { - // TODO(40614): Remove once non-nullability is sound. - checkNotNullable(error, "error"); + AsyncError(:error, :stackTrace) = _interceptUserError(error, stackTrace); return (_AsyncStreamController(null, null, null, null) - .._addError(error, stackTrace ?? AsyncError.defaultStackTrace(error)) + .._addError(error, stackTrace) .._closeUnchecked()) .stream; } diff --git a/sdk/lib/async/stream_controller.dart b/sdk/lib/async/stream_controller.dart index d0580bd5d57..0662cecdef3 100644 --- a/sdk/lib/async/stream_controller.dart +++ b/sdk/lib/async/stream_controller.dart @@ -608,15 +608,8 @@ abstract class _StreamController implements _StreamControllerBase { /// Send or enqueue an error event. void addError(Object error, [StackTrace? stackTrace]) { - checkNotNullable(error, "error"); if (!_mayAddEvent) throw _badEventState(); - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); - if (replacement != null) { - error = replacement.error; - stackTrace = replacement.stackTrace; - } else { - stackTrace ??= AsyncError.defaultStackTrace(error); - } + AsyncError(:error, :stackTrace) = _interceptUserError(error, stackTrace); _addError(error, stackTrace); } diff --git a/sdk/lib/async/stream_impl.dart b/sdk/lib/async/stream_impl.dart index 90de9e38ce6..7e04f5efb67 100644 --- a/sdk/lib/async/stream_impl.dart +++ b/sdk/lib/async/stream_impl.dart @@ -301,6 +301,7 @@ class _BufferingStreamSubscription } void _addError(Object error, StackTrace stackTrace) { + _trySetStackTrace(error, stackTrace); if (_isCanceled) return; if (_canFire) { _sendError(error, stackTrace); // Reports cancel after sending. diff --git a/sdk/lib/async/stream_pipe.dart b/sdk/lib/async/stream_pipe.dart index cd707be13e4..f2e341c331b 100644 --- a/sdk/lib/async/stream_pipe.dart +++ b/sdk/lib/async/stream_pipe.dart @@ -9,13 +9,11 @@ _runUserCode(T userCode(), onSuccess(T value), onError(Object error, StackTrace stackTrace)) { try { onSuccess(userCode()); - } catch (e, s) { - AsyncError? replacement = Zone.current.errorCallback(e, s); - if (replacement == null) { - onError(e, s); + } catch (error, stackTrace) { + AsyncError? replacement = _interceptError(error, stackTrace); + if (replacement != null) { + onError(replacement.error, replacement.stackTrace); } else { - var error = replacement.error; - var stackTrace = replacement.stackTrace; onError(error, stackTrace); } } @@ -35,7 +33,7 @@ void _cancelAndError(StreamSubscription subscription, _Future future, void _cancelAndErrorWithReplacement(StreamSubscription subscription, _Future future, Object error, StackTrace stackTrace) { - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); + AsyncError? replacement = _interceptError(error, stackTrace); if (replacement != null) { error = replacement.error; stackTrace = replacement.stackTrace; @@ -168,7 +166,7 @@ class _ForwardingStreamSubscription void _addErrorWithReplacement( _EventSink sink, Object error, StackTrace stackTrace) { - AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); + var replacement = _interceptError(error, stackTrace); if (replacement != null) { error = replacement.error; stackTrace = replacement.stackTrace; diff --git a/sdk/lib/async/stream_transformers.dart b/sdk/lib/async/stream_transformers.dart index 9193071ce6d..ee4ce3c33ea 100644 --- a/sdk/lib/async/stream_transformers.dart +++ b/sdk/lib/async/stream_transformers.dart @@ -213,7 +213,6 @@ class _HandlerEventSink implements EventSink { } void addError(Object error, [StackTrace? stackTrace]) { - checkNotNullable(error, "error"); var sink = _sink; if (sink == null) { throw StateError("Sink is closed"); diff --git a/sdk/lib/async/zone.dart b/sdk/lib/async/zone.dart index fb059273514..804892db9d4 100644 --- a/sdk/lib/async/zone.dart +++ b/sdk/lib/async/zone.dart @@ -154,20 +154,37 @@ typedef RegisterBinaryCallbackHandler /// The type of a custom [Zone.errorCallback] implementation function. /// -/// Receives the [Zone] that the handler was registered on as [self], -/// a delegate forwarding to the handlers of [self]'s parent zone as [parent], -/// and the current zone where the error was uncaught as [zone], -/// which will have [self] as a parent zone. -/// /// The [error] and [stackTrace] are the error and stack trace /// passed to [Zone.errorCallback] of [zone]. /// -/// The function should return either `null` if it doesn't want -/// to replace the original error and stack trace, -/// or an [AsyncError] containing a replacement error and stack trace -/// which will be used to replace the originals. +/// The function will be called when a synchronous error becomes an +/// asynchronous error, either by being caught, for example by a `Future.then` +/// callback throwing, or when used to create an asynchronous error +/// programmatically, for example using `Future.error` or `Completer.complete`. /// -/// The error callback handler must not throw. +/// If the function does not want to replace the error or stack trace, +/// it should just return `parent.errorCallback(zone, error, stackTrace)`, +/// giving the parent zone the chance to intercept. +/// +/// If the function does want to replace the error and/or stack trace, +/// say with `error2` and `stackTrace2`, it should still allow the +/// parent zone to intercept those errors, for examples as: +/// ```dart +/// return parent.errorCallback(zone, error, stackTrace) ?? +/// AsyncError(error, stackTrace); +/// ``` +/// +/// The function returns either `null` if the original error and stack trace +/// is unchanged, avoiding any allocation in the most common case, +/// or an [AsyncError] containing a replacement error and stack trace +/// which will be used in place of the originals as the asynchronous error. +/// +/// The [self] [Zone] is the zone the handler was registered on, +/// the [parent] delegate forwards to the handlers of [self]'s parent zone, +/// and [zone] is the current zone where the error was uncaught, +/// which will have [self] as an ancestor zone. +/// +/// The error callback handler **must not** throw. /// /// The function must only access zone-related functionality through /// [self], [parent] or [zone]. @@ -989,7 +1006,6 @@ base class _ZoneDelegate implements ZoneDelegate { } AsyncError? errorCallback(Zone zone, Object error, StackTrace? stackTrace) { - checkNotNullable(error, "error"); var implementation = _delegationTarget._errorCallback; _Zone implZone = implementation.zone; if (identical(implZone, _rootZone)) return null; @@ -1338,7 +1354,6 @@ base class _CustomZone extends _Zone { } AsyncError? errorCallback(Object error, StackTrace? stackTrace) { - checkNotNullable(error, "error"); var implementation = this._errorCallback; final _Zone implementationZone = implementation.zone; if (identical(implementationZone, _rootZone)) return null; @@ -1745,7 +1760,6 @@ R runZoned(R body(), {Map? zoneValues, ZoneSpecification? zoneSpecification, @Deprecated("Use runZonedGuarded instead") Function? onError}) { - checkNotNullable(body, "body"); if (onError != null) { // TODO: Remove this when code have been migrated off using [onError]. if (onError is! void Function(Object, StackTrace)) { @@ -1789,8 +1803,6 @@ R runZoned(R body(), @Since("2.8") R? runZonedGuarded(R body(), void onError(Object error, StackTrace stack), {Map? zoneValues, ZoneSpecification? zoneSpecification}) { - checkNotNullable(body, "body"); - checkNotNullable(onError, "onError"); _Zone parentZone = Zone._current; HandleUncaughtErrorHandler errorHandler = (Zone self, ZoneDelegate parent, Zone zone, Object error, StackTrace stackTrace) { diff --git a/tests/lib/async/error_stack_test.dart b/tests/lib/async/error_stack_test.dart new file mode 100644 index 00000000000..b85d6ded4bc --- /dev/null +++ b/tests/lib/async/error_stack_test.dart @@ -0,0 +1,157 @@ +// Copyright (c) 2024, 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:async'; + +// Checks that an `Error` object that is "thrown" through async functionality +// gets a stack trace set. + +import 'package:expect/expect.dart'; +import 'package:async_helper/async_helper.dart'; + +main() async { + asyncStart(); + + for ((String, Object, StackTrace, Object, String, AsyncError?) + Function() setUp in [ + () { + // Without error callback. Inject and expect same fresh error. + var error = StateError("Quo!"); + var stack = StackTrace.fromString("Secret stack"); + return ("", error, stack, error, stack.toString(), null); + }, + () { + // With error callback, same error, new stack. + var error = StateError("Quo"); + var stack = StackTrace.fromString("Secret stack"); + var stack2 = StackTrace.fromString("Other stack"); + var errorCallback = AsyncError(error, stack2); + return ( + "errorCallback: fresh both", + error, + stack, + error, + stack2.toString(), + errorCallback + ); + }, + () { + // With error callback, new error, stack. + var error = StateError("Quo"); + var stack = StackTrace.fromString("Secret stack"); + var error2 = StateError("Quid"); + var stack2 = StackTrace.fromString("Other stack"); + var errorCallback = AsyncError(error2, stack2); + return ( + "errorCallback: fresh stack", + error, + stack, + error2, + stack2.toString(), + errorCallback + ); + }, + ]) { + Future test(String name, + Future<(Object, StackTrace)> Function(Object, StackTrace) body) { + var (msg, error, stack, expectError, expectStackString, errorCallback) = + setUp(); + print("Test: $name${msg.isNotEmpty ? ", $msg" : msg}"); + + return asyncTest(() => runZoned(() => body(error, stack), + zoneSpecification: ZoneSpecification( + errorCallback: (s, p, z, e, st) => errorCallback)).then((es) { + var (e, s) = es; + Expect.identical(expectError, e, name); + Expect.equals(expectStackString, s.toString(), name); + Expect.equals( + expectStackString, (e as Error).stackTrace.toString(), name); + })); + } + + // Sanity check: Plain throws. + () { + var (name, error, stack, error2, stack2, errorCallback) = setUp(); + try { + Error.throwWithStackTrace(error, stack); + } catch (e, s) { + Expect.identical(error, e, "Error.throw"); + Expect.equals(stack.toString(), s.toString(), "Error.throw"); + Expect.equals(stack.toString(), (e as Error).stackTrace.toString(), + "Error.throw"); + } + }(); + + // Futures. + + // Immediate error. + await test("Future.error", + (error, stack) => futureError(Future.error(error, stack))); + + // Through controller, async. + await test("Completer().completeError", (error, stack) { + var completer = Completer(); + completer.completeError(error, stack); + return futureError(completer.future); + }); + + // Through controller, sync. + await test("Completer.sync().completeError", (error, stack) { + var completer = Completer.sync(); + var future = completer.future..ignore(); + completer.completeError(error, stack); + return futureError(future); + }); + + // Streams. + + // Singleton error. + await test("Stream.error", + (error, stack) => streamError(Stream.error(error, stack))); + + // Controller errors. + for (var broadcast in [false, true]) { + for (var sync in [false, true]) { + await test( + "Stream${broadcast ? ".broadcast" : ""}${sync ? "(sync)" : ""}", + (error, stack) { + var controller = broadcast + ? StreamController.broadcast(sync: sync) + : StreamController(sync: sync); + var future = streamError(controller.stream)..ignore(); + controller.addError(error, stack); + return future; + }); + } + } + } + + asyncEnd(); +} + +// -------------------------------------------------------------------- +// Helper functions + +(Object, StackTrace) captureError(Object e, StackTrace s) => (e, s); + +(Object, StackTrace) fail(_) { + Expect.fail("No error from future") as Never; +} + +/// Captures error of future. +Future<(Object, StackTrace)> futureError(Future future) => + future.then(fail, onError: captureError); + +/// Captures first error of stream. +Future<(Object, StackTrace)> streamError(Stream stream) { + var c = Completer<(Object, StackTrace)>(); + var sub = stream.listen((_) { + // No values expected. + }, onError: (Object e, StackTrace s) { + c.complete((e, s)); + }, onDone: () { + Expect.fail("No error from stream"); + }, cancelOnError: true); + return c.future; +} diff --git a/tests/lib/async/future_error_test.dart b/tests/lib/async/future_error_test.dart deleted file mode 100644 index 47f0f154a82..00000000000 --- a/tests/lib/async/future_error_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2020, 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:async'; - -import 'package:expect/expect.dart'; - -main() { - // The error cannot be null. - Expect.throwsTypeError(() { - Future.error(null as dynamic); - }); -} diff --git a/tests/lib/async/stream_controller_add_error_test.dart b/tests/lib/async/stream_controller_add_error_test.dart deleted file mode 100644 index aea929449ee..00000000000 --- a/tests/lib/async/stream_controller_add_error_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2020, 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:async'; - -import 'package:expect/expect.dart'; - -main() { - // Single-cast async. - var controller = StreamController(); - Expect.throwsTypeError(() { - controller.addError(null as dynamic); - }); - - Expect.throwsTypeError(() { - controller.sink.addError(null as dynamic); - }); - - // Single-cast sync. - controller = StreamController(sync: true); - Expect.throwsTypeError(() { - controller.addError(null as dynamic); - }); - - Expect.throwsTypeError(() { - controller.sink.addError(null as dynamic); - }); - - // Broadcast async. - controller = StreamController.broadcast(); - Expect.throwsTypeError(() { - controller.addError(null as dynamic); - }); - - Expect.throwsTypeError(() { - controller.sink.addError(null as dynamic); - }); - - // Broadcast sync. - controller = StreamController.broadcast(sync: true); - Expect.throwsTypeError(() { - controller.addError(null as dynamic); - }); - - Expect.throwsTypeError(() { - controller.sink.addError(null as dynamic); - }); -} diff --git a/tests/lib/async/zone_async_error_test.dart b/tests/lib/async/zone_async_error_test.dart deleted file mode 100644 index 1d3bf045da3..00000000000 --- a/tests/lib/async/zone_async_error_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2020, 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:async'; - -import 'package:expect/expect.dart'; - -main() { - // The error cannot be null. - Expect.throwsTypeError(() { - AsyncError(null as dynamic, StackTrace.current); - }); -} diff --git a/tests/lib/async/zone_error_callback_test.dart b/tests/lib/async/zone_error_callback_test.dart index 7b81e7e18b5..28bfe32ba33 100644 --- a/tests/lib/async/zone_error_callback_test.dart +++ b/tests/lib/async/zone_error_callback_test.dart @@ -48,9 +48,12 @@ Null expectErrorOnly(e, s) { } AsyncError? replace(self, parent, zone, e, s) { - if (e == "ignore") return null; // For testing handleError throwing. - Expect.identical(error1, e); // Ensure replacement only called once - return new AsyncError(error2, stack2); + if (identical(error1, e)) { + // Give parent chance to replace your replacements. + return parent.errorCallback(zone, error2, stack2) ?? + AsyncError(error2, stack2); + } + return parent.errorCallback(zone, e, s); } var replaceZoneSpec = new ZoneSpecification(errorCallback: replace);