[dartdevc] ensure all throws are wrapped in JS Error (#33331)
This improves the default JS display of exceptions/errors from DDC
compiled code. This gives a better "default" experience if JS code
(or a JS engine, like browers/Node.js) ends up catching Dart exceptions.
Change-Id: Ib2dda6eee710f8b536d5ed7223e0101310a137b3
Reviewed-on: https://dart-review.googlesource.com/c/84446
Commit-Queue: Jenny Messerly <jmesserly@google.com>
Reviewed-by: Vijay Menon <vsm@google.com>
diff --git a/pkg/dev_compiler/lib/src/analyzer/code_generator.dart b/pkg/dev_compiler/lib/src/analyzer/code_generator.dart
index 5ad5f2e..fcc9651 100644
--- a/pkg/dev_compiler/lib/src/analyzer/code_generator.dart
+++ b/pkg/dev_compiler/lib/src/analyzer/code_generator.dart
@@ -105,7 +105,7 @@
Expression _cascadeTarget;
/// The variable for the current catch clause
- SimpleIdentifier _catchParameter;
+ SimpleIdentifier _rethrowParameter;
/// In an async* function, this represents the stream controller parameter.
JS.TemporaryId _asyncStarController;
@@ -754,11 +754,16 @@
}
@override
- visitIsExpression(IsExpression node) {
+ JS.Expression visitIsExpression(IsExpression node) {
+ return _emitIsExpression(
+ node.expression, node.type.type, node.notOperator != null);
+ }
+
+ JS.Expression _emitIsExpression(Expression operand, DartType type,
+ [bool negated = false]) {
// Generate `is` as `dart.is` or `typeof` depending on the RHS type.
JS.Expression result;
- var type = node.type.type;
- var lhs = _visitExpression(node.expression);
+ var lhs = _visitExpression(operand);
var typeofName = jsTypeRep.typeFor(type).primitiveTypeOf;
// Inline primitives other than int (which requires a Math.floor check).
if (typeofName != null && type != types.intType) {
@@ -767,10 +772,7 @@
result = js.call('#.is(#)', [_emitType(type), lhs]);
}
- if (node.notOperator != null) {
- return js.call('!#', result);
- }
- return result;
+ return negated ? js.call('!#', result) : result;
}
/// No-op, typedefs are emitted as their corresponding function type.
@@ -2682,11 +2684,14 @@
}
bool _executesAtTopLevel(AstNode node) {
- var ancestor = node.thisOrAncestorMatching((n) =>
- n is FunctionBody ||
- n is FieldDeclaration && n.staticKeyword == null ||
- n is ConstructorDeclaration && n.constKeyword == null);
- return ancestor == null;
+ for (var n = node.parent; n != null; n = n.parent) {
+ if (n is FunctionBody ||
+ n is FieldDeclaration && n.staticKeyword == null ||
+ n is ConstructorDeclaration && n.constKeyword == null) {
+ return false;
+ }
+ }
+ return true;
}
/// Whether the expression for [type] can be evaluated at this point in the JS
@@ -5387,7 +5392,7 @@
@override
JS.Expression visitRethrowExpression(RethrowExpression node) {
- return runtimeCall('rethrow(#)', _visitExpression(_catchParameter));
+ return runtimeCall('rethrow(#)', _emitSimpleIdentifier(_rethrowParameter));
}
/// Visits a statement, and ensures the resulting AST handles block scope
@@ -5406,7 +5411,7 @@
}
@override
- JS.If visitIfStatement(IfStatement node) {
+ JS.Statement visitIfStatement(IfStatement node) {
return JS.If(_visitTest(node.condition), _visitScope(node.thenStatement),
_visitScope(node.elseStatement));
}
@@ -5537,82 +5542,84 @@
JS.Catch _visitCatch(NodeList<CatchClause> clauses) {
if (clauses == null || clauses.isEmpty) return null;
- // TODO(jmesserly): need a better way to get a temporary variable.
- // This could incorrectly shadow a user's name.
- var savedCatch = _catchParameter;
+ var caughtError = _createTemporary('e', types.dynamicType);
+ var savedRethrow = _rethrowParameter;
+ _rethrowParameter = caughtError;
- var isSingleCatch =
- clauses.length == 1 && clauses.single.exceptionParameter != null;
- if (isSingleCatch) {
- // Special case for a single catch.
- _catchParameter = clauses.single.exceptionParameter;
- } else {
- _catchParameter = _createTemporary('e', types.dynamicType);
- }
+ // If we have more than one catch clause, always create a temporary so we
+ // don't shadow any names.
+ var exceptionParameter =
+ (clauses.length == 1 ? clauses[0].exceptionParameter : null) ??
+ _createTemporary('ex', types.dynamicType);
- JS.Statement catchBody =
- js.statement('throw #;', _emitSimpleIdentifier(_catchParameter));
+ var stackTraceParameter =
+ (clauses.length == 1 ? clauses[0].stackTraceParameter : null) ??
+ (clauses.any((c) => c.stackTraceParameter != null)
+ ? _createTemporary('st', types.dynamicType)
+ : null);
+
+ // Rethrow if the exception type didn't match.
+ JS.Statement catchBody = JS.Throw(_emitSimpleIdentifier(caughtError));
for (var clause in clauses.reversed) {
- catchBody = _catchClauseGuard(clause, catchBody);
+ catchBody = _catchClauseGuard(
+ clause, catchBody, exceptionParameter, stackTraceParameter);
}
+ var catchStatements = [
+ js.statement('let # = #.getThrown(#)', [
+ _emitVariableDef(exceptionParameter),
+ runtimeModule,
+ _emitSimpleIdentifier(caughtError)
+ ]),
+ ];
+ if (stackTraceParameter != null) {
+ catchStatements.add(js.statement('let # = #.stackTrace(#)', [
+ _emitVariableDef(stackTraceParameter),
+ runtimeModule,
+ _emitSimpleIdentifier(caughtError)
+ ]));
+ }
+ catchStatements.add(catchBody);
- var catchVarDecl = _emitSimpleIdentifier(_catchParameter) as JS.Identifier;
- if (isSingleCatch) {
- catchVarDecl..sourceInformation = _nodeStart(_catchParameter);
- }
- _catchParameter = savedCatch;
- return JS.Catch(catchVarDecl, JS.Block([catchBody]));
+ var catchVarDecl = _emitSimpleIdentifier(caughtError) as JS.Identifier;
+ _rethrowParameter = savedRethrow;
+ return JS.Catch(catchVarDecl, JS.Block(catchStatements));
}
- JS.Statement _catchClauseGuard(CatchClause clause, JS.Statement otherwise) {
- var then = visitCatchClause(clause);
-
- // Discard following clauses, if any, as they are unreachable.
- if (clause.exceptionType == null) return then;
-
- // TODO(jmesserly): this is inconsistent with [visitIsExpression], which
- // has special case for typeof.
- var castType = _emitType(clause.exceptionType.type);
-
- return JS.If(
- js.call('#.is(#)', [castType, _emitSimpleIdentifier(_catchParameter)]),
- then,
- otherwise)
- ..sourceInformation = _nodeStart(clause);
- }
-
- /// Visits the catch clause body. This skips the exception type guard, if any.
- /// That is handled in [_visitCatch].
- @override
- JS.Statement visitCatchClause(CatchClause node) {
+ JS.Statement _catchClauseGuard(
+ CatchClause node,
+ JS.Statement otherwise,
+ SimpleIdentifier exceptionParameter,
+ SimpleIdentifier stackTraceParameter) {
var body = <JS.Statement>[];
-
- var savedCatch = _catchParameter;
var vars = HashSet<String>();
+
+ void declareVariable(SimpleIdentifier variable, SimpleIdentifier value) {
+ if (variable == null) return;
+ vars.add(variable.name);
+ if (variable.name != value.name) {
+ body.add(js.statement('let # = #',
+ [visitSimpleIdentifier(variable), _emitSimpleIdentifier(value)]));
+ }
+ }
+
if (node.catchKeyword != null) {
- var name = node.exceptionParameter;
- if (name == _catchParameter) {
- vars.add(name.name);
- } else if (name != null) {
- vars.add(name.name);
- body.add(js.statement('let # = #;',
- [_emitVariableDef(name), _emitSimpleIdentifier(_catchParameter)]));
- _catchParameter = name;
- }
- var stackVar = node.stackTraceParameter;
- if (stackVar != null) {
- vars.add(stackVar.name);
- body.add(js.statement('let # = #.stackTrace(#);', [
- _emitVariableDef(stackVar),
- runtimeModule,
- _emitSimpleIdentifier(name)
- ]));
- }
+ declareVariable(node.exceptionParameter, exceptionParameter);
+ declareVariable(node.stackTraceParameter, stackTraceParameter);
}
body.add(_visitStatement(node.body).toScopedBlock(vars));
- _catchParameter = savedCatch;
- return JS.Statement.from(body);
+ var then = JS.Statement.from(body);
+
+ // Discard following clauses, if any, as they are unreachable.
+ if (node.exceptionType == null ||
+ rules.isSubtypeOf(types.objectType, node.exceptionType.type)) {
+ return then;
+ }
+
+ var condition =
+ _emitIsExpression(exceptionParameter, node.exceptionType.type);
+ return JS.If(condition, then, otherwise)
+ ..sourceInformation = _nodeStart(node);
}
@override
@@ -6266,6 +6273,10 @@
@override
visitAssertInitializer(node) => _unreachable(node);
+ /// Unused, see [_catchClauseGuard].
+ @override
+ visitCatchClause(CatchClause node) => _unreachable(node);
+
/// Not visited, but maybe they should be?
/// See <https://github.com/dart-lang/sdk/issues/29347>
@override
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index 08a8e2b..f934828 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -58,7 +58,7 @@
final _imports = Map<Library, JS.TemporaryId>();
/// The variable for the current catch clause
- VariableDeclaration _catchParameter;
+ VariableDeclaration _rethrowParameter;
/// In an async* function, this represents the stream controller parameter.
JS.TemporaryId _asyncStarController;
@@ -3502,65 +3502,76 @@
JS.Catch _visitCatch(List<Catch> clauses) {
if (clauses.isEmpty) return null;
- var savedCatch = _catchParameter;
+ var caughtError = VariableDeclaration('#e');
+ var savedRethrow = _rethrowParameter;
+ _rethrowParameter = caughtError;
- if (clauses.length == 1 && clauses.single.exception != null) {
- // Special case for a single catch.
- _catchParameter = clauses.single.exception;
- } else {
- _catchParameter = VariableDeclaration('#e');
- }
+ // If we have more than one catch clause, always create a temporary so we
+ // don't shadow any names.
+ var exceptionParameter =
+ (clauses.length == 1 ? clauses[0].exception : null) ??
+ VariableDeclaration('#ex');
- JS.Statement catchBody =
- js.statement('throw #;', _emitVariableRef(_catchParameter));
+ var stackTraceParameter =
+ (clauses.length == 1 ? clauses[0].stackTrace : null) ??
+ (clauses.any((c) => c.stackTrace != null)
+ ? VariableDeclaration('#st')
+ : null);
+
+ JS.Statement catchBody = JS.Throw(_emitVariableRef(caughtError));
for (var clause in clauses.reversed) {
- catchBody = _catchClauseGuard(clause, catchBody);
+ catchBody = _catchClauseGuard(
+ clause, catchBody, exceptionParameter, stackTraceParameter);
}
-
- var catchVarDecl = _emitVariableRef(_catchParameter);
- _catchParameter = savedCatch;
- return JS.Catch(catchVarDecl, catchBody.toBlock());
+ var catchStatements = [
+ js.statement('let # = #.getThrown(#)', [
+ _emitVariableDef(exceptionParameter),
+ runtimeModule,
+ _emitVariableRef(caughtError)
+ ]),
+ ];
+ if (stackTraceParameter != null) {
+ catchStatements.add(js.statement('let # = #.stackTrace(#)', [
+ _emitVariableDef(stackTraceParameter),
+ runtimeModule,
+ _emitVariableRef(caughtError)
+ ]));
+ }
+ catchStatements.add(catchBody);
+ _rethrowParameter = savedRethrow;
+ return JS.Catch(_emitVariableDef(caughtError), JS.Block(catchStatements));
}
- JS.Statement _catchClauseGuard(Catch node, JS.Statement otherwise) {
+ JS.Statement _catchClauseGuard(
+ Catch node,
+ JS.Statement otherwise,
+ VariableDeclaration exceptionParameter,
+ VariableDeclaration stackTraceParameter) {
var body = <JS.Statement>[];
-
- var savedCatch = _catchParameter;
var vars = HashSet<String>();
- if (node.exception != null) {
- var name = node.exception;
- if (name == _catchParameter) {
- vars.add(name.name);
- } else if (name != null) {
- vars.add(name.name);
- body.add(js.statement('let # = #;',
- [_emitVariableDef(name), _emitVariableRef(_catchParameter)]));
- _catchParameter = name;
- }
- var stackTrace = node.stackTrace;
- if (stackTrace != null) {
- vars.add(stackTrace.name);
- body.add(js.statement('let # = #.stackTrace(#);', [
- _emitVariableDef(stackTrace),
- runtimeModule,
- _emitVariableRef(name)
- ]));
+
+ void declareVariable(
+ VariableDeclaration variable, VariableDeclaration value) {
+ if (variable == null) return;
+ vars.add(variable.name);
+ if (variable.name != value.name) {
+ body.add(js.statement('let # = #',
+ [_emitVariableDef(variable), _emitVariableRef(value)]));
}
}
+ declareVariable(node.exception, exceptionParameter);
+ declareVariable(node.stackTrace, stackTraceParameter);
+
body.add(_visitStatement(node.body).toScopedBlock(vars));
- _catchParameter = savedCatch;
var then = JS.Block(body);
+ // Discard following clauses, if any, as they are unreachable.
if (types.isTop(node.guard)) return then;
- // TODO(jmesserly): this is inconsistent with [visitIsExpression], which
- // has special case for typeof.
- return JS.If(
- js.call('#.is(#)',
- [_emitType(node.guard), _emitVariableRef(_catchParameter)]),
- then,
- otherwise)
+ var condition =
+ _emitIsExpression(VariableGet(exceptionParameter), node.guard);
+ return JS.If(condition, then, otherwise)
..sourceInformation = _nodeStart(node);
}
@@ -4730,20 +4741,19 @@
@override
visitIsExpression(IsExpression node) {
+ return _emitIsExpression(node.operand, node.type);
+ }
+
+ JS.Expression _emitIsExpression(Expression operand, DartType type) {
// Generate `is` as `dart.is` or `typeof` depending on the RHS type.
- JS.Expression result;
- var type = node.type;
- var lhs = _visitExpression(node.operand);
+ var lhs = _visitExpression(operand);
var typeofName = _typeRep.typeFor(type).primitiveTypeOf;
// Inline primitives other than int (which requires a Math.floor check).
- if (typeofName != null && type != coreTypes.intClass.rawType) {
- result = js.call('typeof # == #', [lhs, js.string(typeofName, "'")]);
+ if (typeofName != null && type != types.intType) {
+ return js.call('typeof # == #', [lhs, js.string(typeofName, "'")]);
} else {
- // Always go through a runtime helper, because implicit interfaces.
- var castType = _emitType(type);
- result = js.call('#.is(#)', [castType, lhs]);
+ return js.call('#.is(#)', [_emitType(type), lhs]);
}
- return result;
}
@override
@@ -4858,7 +4868,7 @@
@override
visitRethrow(Rethrow node) {
- return runtimeCall('rethrow(#)', _emitVariableRef(_catchParameter));
+ return runtimeCall('rethrow(#)', _emitVariableRef(_rethrowParameter));
}
@override
diff --git a/pkg/dev_compiler/test/sourcemap/ddc_common.dart b/pkg/dev_compiler/test/sourcemap/ddc_common.dart
index 7dbf654..3d187dd 100644
--- a/pkg/dev_compiler/test/sourcemap/ddc_common.dart
+++ b/pkg/dev_compiler/test/sourcemap/ddc_common.dart
@@ -139,7 +139,10 @@
try {
dartMainRunner(main, []);
} catch(e) {
- console.error(e.toString(), dart.stackTrace(e).toString());
+ console.error(e);
+ // d8 does not seem to print the `.stack` property like
+ // node.js and browsers do.
+ console.error(e.stack);
}
""";
}
diff --git a/pkg/dev_compiler/tool/ddb b/pkg/dev_compiler/tool/ddb
index 52fe107..2b5acac 100755
--- a/pkg/dev_compiler/tool/ddb
+++ b/pkg/dev_compiler/tool/ddb
@@ -194,7 +194,7 @@
if (!source_maps) {
console.log('For Dart source maps: npm install source-map-support');
}
- console.error(e.toString(), sdk.dart.stackTrace(e).toString());
+ console.error(e);
process.exit(1);
}
''';
@@ -216,7 +216,7 @@
_isolate_helper.startRootIsolate(() => {}, []);
main();
} catch(e) {
- console.error(e.toString(), dart.stackTrace(e).toString());
+ console.error(e);
}
''';
var d8File = '$libRoot/$basename.d8.js';
diff --git a/pkg/dev_compiler/tool/ddc b/pkg/dev_compiler/tool/ddc
index bacfff7..6717a41 100755
--- a/pkg/dev_compiler/tool/ddc
+++ b/pkg/dev_compiler/tool/ddc
@@ -113,8 +113,7 @@
if (!source_maps) {
console.log('For Dart source maps: npm install source-map-support');
}
- let toString = sdk.dart.toString;
- console.error(toString(e), toString(sdk.dart.stackTrace(e)));
+ console.error(e);
process.exit(1);
}" \
> $LIBROOT/$BASENAME.run.js
diff --git a/pkg/dev_compiler/tool/input_sdk/patch/async_patch.dart b/pkg/dev_compiler/tool/input_sdk/patch/async_patch.dart
index 62b34428..983bbcd 100644
--- a/pkg/dev_compiler/tool/input_sdk/patch/async_patch.dart
+++ b/pkg/dev_compiler/tool/input_sdk/patch/async_patch.dart
@@ -4,8 +4,7 @@
// Patch file for the dart:async library.
-import 'dart:_js_helper'
- show notNull, patch, setTraceForException, ReifyFunctionTypes;
+import 'dart:_js_helper' show notNull, patch, ReifyFunctionTypes;
import 'dart:_isolate_helper'
show TimerImpl, global, leaveJsAsync, enterJsAsync;
import 'dart:_foreign_helper' show JS, JSExportName;
@@ -30,7 +29,7 @@
_async<T>(Function() initGenerator) {
var iter;
Object Function(Object) onValue;
- Object Function(Object) onError;
+ Object Function(Object, StackTrace) onError;
onAwait(Object value) {
_Future f;
@@ -60,8 +59,9 @@
//
// In essence, we are giving the code inside the generator a chance to
// use try-catch-finally.
- onError = (value) {
- var iteratorResult = JS('', '#.throw(#)', iter, value);
+ onError = (value, stackTrace) {
+ var iteratorResult = JS(
+ '', '#.throw(#)', iter, dart.createErrorWithStack(value, stackTrace));
value = JS('', '#.value', iteratorResult);
return JS('bool', '#.done', iteratorResult) ? value : onAwait(value);
};
@@ -69,7 +69,7 @@
var zone = Zone.current;
if (!identical(zone, _rootZone)) {
onValue = zone.registerUnaryCallback(onValue);
- onError = zone.registerUnaryCallback(onError);
+ onError = zone.registerBinaryCallback(onError);
}
var asyncFuture = _Future<T>();
@@ -243,8 +243,7 @@
@patch
void _rethrow(Object error, StackTrace stackTrace) {
- setTraceForException(error, stackTrace);
- dart.throw_(error);
+ JS('', 'throw #', dart.createErrorWithStack(error, stackTrace));
}
/// Used by the compiler to implement `async*` functions.
@@ -328,9 +327,12 @@
if (_handleErrorCallback == null) {
_handleErrorCallback = (error, StackTrace stackTrace) {
try {
- JS('', '#.throw(#)', jsIterator, error);
- } catch (e) {
- addError(e, stackTrace);
+ JS('', '#.throw(#)', jsIterator,
+ dart.createErrorWithStack(error, stackTrace));
+ } catch (e, newStack) {
+ // The generator didn't catch the error, or it threw a new one.
+ // Make sure to propagate the new error.
+ addError(e, newStack);
}
};
var zone = Zone.current;
diff --git a/pkg/dev_compiler/tool/input_sdk/patch/core_patch.dart b/pkg/dev_compiler/tool/input_sdk/patch/core_patch.dart
index bfaf8ac..581bc3f 100644
--- a/pkg/dev_compiler/tool/input_sdk/patch/core_patch.dart
+++ b/pkg/dev_compiler/tool/input_sdk/patch/core_patch.dart
@@ -10,7 +10,6 @@
patch,
checkInt,
getRuntimeType,
- getTraceFromException,
LinkedMap,
JSSyntaxRegExp,
NoInline,
@@ -207,7 +206,7 @@
}
@patch
- StackTrace get stackTrace => getTraceFromException(this);
+ StackTrace get stackTrace => dart.stackTraceForError(this);
}
@patch
@@ -745,7 +744,7 @@
@patch
@NoInline()
static StackTrace get current {
- return getTraceFromException(JS('', 'new Error()'));
+ return dart.stackTrace(JS('', 'Error()'));
}
}
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/errors.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/errors.dart
index a7606a4..7c96274 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/errors.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/errors.dart
@@ -84,3 +84,197 @@
return "Type '${typeName(from)}' is not a subtype of "
"expected type '${typeName(to)}'.";
}
+
+/// The symbol that references the thrown Dart Object (typically but not
+/// necessarily an [Error] or [Exception]), used by the [exception] function.
+final Object _thrownValue = JS('', 'Symbol("_thrownValue")');
+
+/// For a Dart [Error], this provides access to the JS Error object that
+/// contains the stack trace if the error was thrown.
+final Object _jsError = JS('', 'Symbol("_jsError")');
+
+/// Gets the thrown Dart Object from an [error] caught by a JS catch.
+///
+/// If the throw originated in Dart, the result will typically be an [Error]
+/// or [Exception], but it could be any Dart object.
+///
+/// If the throw originated in JavaScript, then there is not a corresponding
+/// Dart value, so we just return the error object.
+Object getThrown(Object error) {
+ if (error != null) {
+ // Get the Dart thrown value, if any.
+ var value = JS('', '#[#]', error, _thrownValue);
+ if (value != null) return value;
+ }
+ // Otherwise return the original object.
+ return error;
+}
+
+final _stackTrace = JS('', 'Symbol("_stackTrace")');
+
+/// Returns the stack trace from an [error] caught by a JS catch.
+///
+/// If the throw originated in Dart, we should always have JS Error
+/// (see [throw_]) so we can create a Dart [StackTrace] from that (or return a
+/// previously created instance).
+///
+/// If the throw originated in JavaScript and was an `Error`, then we can get
+/// the corresponding stack trace the same way we do for Dart throws. If the
+/// throw object was not an Error, then we don't have a JS trace, so we create
+/// one here.
+StackTrace stackTrace(Object error) {
+ if (JS<bool>('!', '!(# instanceof Error)', error)) {
+ // We caught something that isn't a JS Error.
+ //
+ // We should only hit this path when a non-Error was thrown from JS. In
+ // case, there is no stack trace available, so create one here.
+ return _StackTrace.missing(error);
+ }
+
+ // If we've already created the Dart stack trace object, return it.
+ StackTrace trace = JS('', '#[#]', error, _stackTrace);
+ if (trace != null) return trace;
+
+ // Otherwise create the Dart stack trace (by parsing the JS stack), and
+ // cache it so we don't repeat the parsing/allocation.
+ return JS('', '#[#] = #', error, _stackTrace, _StackTrace(error));
+}
+
+StackTrace stackTraceForError(Error error) {
+ return stackTrace(JS('', '#[#]', error, _jsError));
+}
+
+/// 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
+/// unwrapped value returned by [getThrown].
+@JSExportName('rethrow')
+void rethrow_(Object error) {
+ JS('', 'throw #', error);
+}
+
+/// Subclass of JS `Error` that wraps a thrown Dart object, and evaluates the
+/// message lazily by calling `toString()` on the wrapped Dart object.
+///
+/// Also creates a pointer from the Dart [Error] to the JS Error
+/// (via [_jsError]). This is used to implement [Error.stackTrace].
+///
+/// TODO(jmesserly): Dart Errors should simply be JS Errors.
+final Object DartError = JS(
+ '!',
+ '''class DartError extends Error {
+ constructor(error) {
+ super();
+ this[#] = error;
+ if (error instanceof # && error[#] == null) error[#] = this;
+ }
+ get message() {
+ return #(this[#]);
+ }
+ }''',
+ _thrownValue,
+ Error,
+ _jsError,
+ _jsError,
+ _toString,
+ _thrownValue);
+
+/// Subclass of [DartError] for cases where we're rethrowing with a different,
+/// original Dart StackTrace object.
+///
+/// This includes the original stack trace in the JS Error message so it doesn't
+/// get lost if the exception reaches JS.
+final Object RethrownDartError = JS(
+ '!',
+ '''class RethrownDartError extends # {
+ constructor(error, stackTrace) {
+ super(error);
+ this[#] = stackTrace;
+ }
+ get message() {
+ return super.message + "\\n " + #(this[#]) + "\\n";
+ }
+ }''',
+ DartError,
+ _stackTrace,
+ _toString,
+ _stackTrace);
+
+/// Implements `throw` of [exception], allowing for throw in an expression
+/// context, and capturing the current stack trace.
+@JSExportName('throw')
+void throw_(Object exception) {
+ /// Wrap the object so we capture a new stack trace, and so it will print
+ /// nicely from JS, as if it were a normal JS error.
+ JS('', 'throw new #(#)', DartError, exception);
+}
+
+/// Returns a JS error for throwing the Dart [exception] Object and using the
+/// provided stack [trace].
+///
+/// This is used by dart:async to rethrow unhandled errors in [Zone]s, and by
+/// `async`/`async*` to rethrow errors from Futures/Streams into the generator
+/// (so a try/catch in there can catch it).
+///
+/// If the exception and trace originated from the same Dart throw, then we can
+/// simply return the original JS Error. Otherwise, we have to create a new JS
+/// Error. The new error will have the correct Dart trace, but it will not have
+/// the correct JS stack trace (visible if JavaScript ends up handling it). To
+/// fix that, we use [RethrownDartError] to preserve the Dart trace and make
+/// sure it gets displayed in the JS error message.
+Object createErrorWithStack(Object exception, StackTrace trace) {
+ if (trace is _StackTrace) {
+ /// Optimization: if this stack trace and exception already have a matching
+ /// Error, we can just rethrow it.
+ var originalError = trace._jsError;
+ if (identical(exception, getThrown(originalError))) {
+ return originalError;
+ }
+ }
+ var error = JS('', 'new #(#)', RethrownDartError, exception);
+ JS('', '#[#] = #', error, _stackTrace, trace);
+ return error;
+}
+
+// This is a utility function: it is only intended to be called from dev
+// tools.
+void stackPrint(Object error) {
+ JS('', 'console.log(#.stack ? #.stack : "No stack trace for: " + #)', error,
+ error, error);
+}
+
+class _StackTrace implements StackTrace {
+ final Object _jsError;
+ final Object _jsObjectMissingTrace;
+ String _trace;
+
+ _StackTrace(this._jsError) : _jsObjectMissingTrace = null;
+
+ _StackTrace.missing(Object caughtObj)
+ : _jsObjectMissingTrace = caughtObj != null ? caughtObj : 'null',
+ _jsError = JS('', 'Error()');
+
+ String toString() {
+ if (_trace != null) return _trace;
+
+ var e = _jsError;
+ String trace = '';
+ if (e != null && JS('bool', 'typeof # === "object"', e)) {
+ trace = e is NativeError ? e.dartStack() : JS<String>('', '#.stack', e);
+ if (trace != null && stackTraceMapper != null) {
+ trace = stackTraceMapper(trace);
+ }
+ }
+ if (trace.isEmpty || _jsObjectMissingTrace != null) {
+ String jsToString;
+ try {
+ jsToString = JS('', '"" + #', _jsObjectMissingTrace);
+ } catch (_) {
+ jsToString = '<error converting JS object to string>';
+ }
+ trace = 'Non-error `$jsToString` thrown by JS does not have stack trace.'
+ '\nCaught in Dart at:\n\n$trace';
+ }
+ return _trace = trace;
+ }
+}
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/operations.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/operations.dart
index 9c7142d..5aed9a6 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/operations.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/operations.dart
@@ -530,76 +530,6 @@
return dtest(value);
}
-/// Store a JS error for an exception. For non-primitives, we store as an
-/// expando. For primitive, we use a side cache. To limit memory leakage, we
-/// only keep the last [_maxTraceCache] entries.
-final _error = JS('', 'Symbol("_error")');
-Map _primitiveErrorCache;
-const _maxErrorCache = 10;
-
-bool _isJsError(exception) {
- return JS('!', '#.Error != null && # instanceof #.Error', global_, exception,
- global_);
-}
-
-// Record/return the JS error for an exception. If an error was already
-// recorded, prefer that to [newError].
-recordJsError(exception, [newError]) {
- if (_isJsError(exception)) return exception;
-
- var useExpando =
- exception != null && JS<bool>('!', 'typeof # == "object"', exception);
- var error;
- if (useExpando) {
- error = JS('', '#[#]', exception, _error);
- } else {
- if (_primitiveErrorCache == null) _primitiveErrorCache = {};
- error = _primitiveErrorCache[exception];
- }
- if (error != null) return error;
- if (newError != null) {
- error = newError;
- } else {
- // We should only hit this path when a non-Error was thrown from JS. In
- // case, there is no stack trace on the exception, so we create one:
- error = JS('', 'new Error()');
- }
- if (useExpando) {
- JS('', '#[#] = #', exception, _error, error);
- } else {
- _primitiveErrorCache[exception] = error;
- if (_primitiveErrorCache.length > _maxErrorCache) {
- _primitiveErrorCache.remove(_primitiveErrorCache.keys.first);
- }
- }
- return error;
-}
-
-@JSExportName('throw')
-throw_(obj) {
- // Note, we create the error here to avoid the extra frame.
- // package:stack_trace and tests appear to assume this. We could fix use
- // cases instead, but we're already on the exceptional path here.
- recordJsError(obj, JS('', 'new Error()'));
- JS('', 'throw #', obj);
-}
-
-@JSExportName('rethrow')
-rethrow_(obj) {
- JS('', 'throw #', obj);
-}
-
-// This is a utility function: it is only intended to be called from dev
-// tools.
-stackPrint(exception) {
- var error = recordJsError(exception);
- JS('', 'console.log(#.stack ? #.stack : "No stack trace for: " + #)', error,
- error, error);
-}
-
-// Forward to dart:_js_helper to create a _StackTrace object.
-stackTrace(exception) => getTraceFromException(exception);
-
final _value = JS('', 'Symbol("_value")');
///
diff --git a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/runtime.dart b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/runtime.dart
index 82289ddd..116464a 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/runtime.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/ddc_runtime/runtime.dart
@@ -8,8 +8,9 @@
import 'dart:async';
import 'dart:collection';
+import 'dart:_debugger' show stackTraceMapper, trackCall;
import 'dart:_foreign_helper' show JS, JSExportName, rest, spread;
-import 'dart:_interceptors' show JSArray, jsNull, JSFunction;
+import 'dart:_interceptors' show JSArray, jsNull, JSFunction, NativeError;
import 'dart:_internal' as internal show Symbol;
import 'dart:_js_helper'
show
@@ -17,7 +18,6 @@
BooleanConversionAssertionError,
CastErrorImpl,
DartIterator,
- getTraceFromException,
TypeErrorImpl,
JsLinkedHashMap,
ImmutableMap,
@@ -25,7 +25,6 @@
ReifyFunctionTypes,
NoReifyGeneric,
notNull;
-import 'dart:_debugger' show trackCall;
export 'dart:_debugger' show getDynamicStats, clearDynamicStats, trackCall;
diff --git a/pkg/dev_compiler/tool/input_sdk/private/interceptors.dart b/pkg/dev_compiler/tool/input_sdk/private/interceptors.dart
index de5ca6e..fb34f25 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/interceptors.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/interceptors.dart
@@ -138,7 +138,7 @@
return stack;
}
- StackTrace get stackTrace => getTraceFromException(this);
+ StackTrace get stackTrace => dart.stackTrace(this);
String toString() {
String message = JS('!', '#.message', this);
@@ -211,7 +211,7 @@
// it to be picked up as an extension type.
@JsPeerInterface(name: 'RangeError')
class JSRangeError extends Interceptor implements ArgumentError {
- StackTrace get stackTrace => getTraceFromException(this);
+ StackTrace get stackTrace => dart.stackTrace(this);
get invalidValue => null;
get name => null;
diff --git a/pkg/dev_compiler/tool/input_sdk/private/js_helper.dart b/pkg/dev_compiler/tool/input_sdk/private/js_helper.dart
index c9533c2..a8672f1 100644
--- a/pkg/dev_compiler/tool/input_sdk/private/js_helper.dart
+++ b/pkg/dev_compiler/tool/input_sdk/private/js_helper.dart
@@ -6,8 +6,6 @@
import 'dart:collection';
-import 'dart:_debugger' show stackTraceMapper;
-
import 'dart:_foreign_helper' show JS, JS_STRING_CONCAT, JSExportName;
import 'dart:_interceptors';
@@ -579,53 +577,6 @@
}
/**
- * Called by generated code to fetch the stack trace from a Dart
- * exception. Should never return null.
- */
-final _stackTrace = JS('', 'Symbol("_stackTrace")');
-StackTrace getTraceFromException(exception) {
- var error = dart.recordJsError(exception);
- StackTrace trace = JS('', '#[#]', error, _stackTrace);
- if (trace != null) return trace;
- trace = _StackTrace(error);
- JS('', '#[#] = #', error, _stackTrace, trace);
- return trace;
-}
-
-/**
- * Called on rethrow to (potentially) set a different trace.
- */
-void setTraceForException(exception, StackTrace trace) {
- var error = dart.recordJsError(exception);
- JS('', '#[#] = #', error, _stackTrace, trace);
-}
-
-class _StackTrace implements StackTrace {
- var _exception;
- String _trace;
-
- _StackTrace(this._exception);
-
- String toString() {
- if (_trace != null) return _trace;
-
- String trace;
- if (JS('bool', '# !== null', _exception) &&
- JS('bool', 'typeof # === "object"', _exception)) {
- if (_exception is NativeError) {
- trace = _exception.dartStack();
- } else {
- trace = JS("", r"#.stack", _exception);
- }
- if (trace != null && stackTraceMapper != null) {
- trace = stackTraceMapper(trace);
- }
- }
- return _trace = (trace == null) ? '' : trace;
- }
-}
-
-/**
* Called by generated code to build a map literal. [keyValuePairs] is
* a list of key, value, key, value, ..., etc.
*/
diff --git a/tests/language_2/language_2_dartdevc.status b/tests/language_2/language_2_dartdevc.status
index 16c2e8d..a2fea50 100644
--- a/tests/language_2/language_2_dartdevc.status
+++ b/tests/language_2/language_2_dartdevc.status
@@ -340,7 +340,6 @@
const_switch_test/04: RuntimeError # Ints and doubles are unified.; Expect.equals(expected: <1>, actual: <1.0>) fails.
constructor12_test: RuntimeError # Issue 29920; ReferenceError: JSArrayOfT is not defined
ct_const_test: RuntimeError # Issue 2992; RangeError: Maximum call stack size exceeded
-custom_await_stack_trace_test: RuntimeError # Issue 29920; Uncaught Expect.equals(at index 0: Expected <Blah \x0ABloop\x0ABleep\x0A...>
cyclic_type2_test: RuntimeError # Issue 29920; Uncaught ReferenceError: V is not defined
cyclic_type_test/02: RuntimeError # Issue 29920; Uncaught RangeError: Maximum call stack size exceeded
cyclic_type_test/03: RuntimeError # Issue 29920; Uncaught ReferenceError: U is not defined