[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