Bug 911216 - Part 14: Add support for tracking unhandled promise rejections, exposed through a JSAPI function. r=efaust
authorTill Schneidereit <till@tillschneidereit.net>
Tue, 22 Mar 2016 16:22:23 +0100
changeset 340201 f0b339198f151607d257d02679a839d0b40dfe43
parent 340200 96cef64eadc3ec04504d52834c300bdb0d202fb5
child 340202 c83647796df591e968223315b8779b1008f42b4b
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersefaust
bugs911216
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 911216 - Part 14: Add support for tracking unhandled promise rejections, exposed through a JSAPI function. r=efaust
js/src/builtin/Promise.cpp
js/src/builtin/Promise.h
js/src/builtin/Promise.js
js/src/builtin/SelfHostingDefines.h
js/src/jsapi.cpp
js/src/jsapi.h
js/src/shell/js.cpp
js/src/tests/ecma_6/Promise/promise-rejection-tracking.js
js/src/vm/Debugger.cpp
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
js/src/vm/SelfHosting.cpp
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -35,17 +35,23 @@ static const JSFunctionSpec promise_stat
     JS_FS_END
 };
 
 static const JSPropertySpec promise_static_properties[] = {
     JS_SELF_HOSTED_SYM_GET(species, "Promise_static_get_species", 0),
     JS_PS_END
 };
 
-// ES6, 25.4.3.1. steps 3-11.
+static Value
+Now()
+{
+    return JS::TimeValue(JS::TimeClip(static_cast<double>(PRMJ_Now()) / PRMJ_USEC_PER_MSEC));
+}
+
+// ES2016, February 12 draft, 25.4.3.1. steps 3-11.
 PromiseObject*
 PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto /* = nullptr */)
 {
     MOZ_ASSERT(executor->isCallable());
 
     RootedObject usedProto(cx, proto);
     bool wrappedProto = false;
     // If the proto is wrapped, that means the current function is running
@@ -68,43 +74,46 @@ PromiseObject::create(JSContext* cx, Han
         // All state stored in a Promise's fixed slots must be created in the
         // same compartment, so we get all of that out of the way here.
         // (Except for the resolution functions, which are created below.)
         mozilla::Maybe<AutoCompartment> ac;
         if (wrappedProto)
             ac.emplace(cx, usedProto);
 
         promise = &NewObjectWithClassProto(cx, &class_, usedProto)->as<PromiseObject>();
-
-        // Step 4.
         if (!promise)
             return nullptr;
 
-        // Step 5.
+        // Step 4.
         promise->setFixedSlot(PROMISE_STATE_SLOT, Int32Value(PROMISE_STATE_PENDING));
 
-        // Step 6.
+        // Step 5.
         RootedArrayObject reactions(cx, NewDenseEmptyArray(cx));
         if (!reactions)
             return nullptr;
         promise->setFixedSlot(PROMISE_FULFILL_REACTIONS_SLOT, ObjectValue(*reactions));
 
-        // Step 7.
+        // Step 6.
         reactions = NewDenseEmptyArray(cx);
         if (!reactions)
             return nullptr;
         promise->setFixedSlot(PROMISE_REJECT_REACTIONS_SLOT, ObjectValue(*reactions));
 
+        // Step 7.
+        promise->setFixedSlot(PROMISE_IS_HANDLED_SLOT,
+                              Int32Value(PROMISE_IS_HANDLED_STATE_UNHANDLED));
+
+        // Store an allocation stack so we can later figure out what the
+        // control flow was for some unexpected results. Frightfully expensive,
+        // but oh well.
         RootedObject stack(cx);
         if (!JS::CaptureCurrentStack(cx, &stack, 0))
             return nullptr;
-        promise->setFixedSlot(PROMISE_ALLOCATION_SITE_SLOT, ObjectValue(*stack));
-        Value now = JS::TimeValue(JS::TimeClip(static_cast<double>(PRMJ_Now()) /
-                                               PRMJ_USEC_PER_MSEC));
-        promise->setFixedSlot(PROMISE_ALLOCATION_TIME_SLOT, now);
+        promise->setFixedSlot(PROMISE_ALLOCATION_SITE_SLOT, ObjectOrNullValue(stack));
+        promise->setFixedSlot(PROMISE_ALLOCATION_TIME_SLOT, Now());
     }
 
     RootedValue promiseVal(cx, ObjectValue(*promise));
     if (wrappedProto && !cx->compartment()->wrap(cx, &promiseVal))
         return nullptr;
 
     // Step 8.
     // The resolving functions are created in the compartment active when the
@@ -176,36 +185,37 @@ PromiseObject::create(JSContext* cx, Han
 
         args[0].set(exceptionVal);
 
         // |rejectVal| is unused after this, so we can safely write to it.
         if (!Call(cx, rejectVal, UndefinedHandleValue, args, &rejectVal))
             return nullptr;
     }
 
+    // Let the Debugger know about this Promise.
     JS::dbg::onNewPromise(cx, promise);
 
     // Step 11.
     return promise;
 }
 
 namespace {
 // Generator used by PromiseObject::getID.
 mozilla::Atomic<uint64_t> gIDGenerator(0);
 } // namespace
 
-double
+uint64_t
 PromiseObject::getID()
 {
     Value idVal(getReservedSlot(PROMISE_ID_SLOT));
     if (idVal.isUndefined()) {
         idVal.setDouble(++gIDGenerator);
         setReservedSlot(PROMISE_ID_SLOT, idVal);
     }
-    return idVal.toNumber();
+    return uint64_t(idVal.toNumber());
 }
 
 /**
  * Returns all promises that directly depend on this one. That means those
  * created by calling `then` on this promise, or the promise returned by
  * `Promise.all(iterable)` or `Promise.race(iterable)`, with this promise
  * being a member of the passed-in `iterable`.
  *
@@ -386,16 +396,37 @@ PromiseObject::reject(JSContext* cx, Han
     FixedInvokeArgs<1> args(cx);
 
     args[0].set(rejectionValue);
 
     RootedValue dummy(cx);
     return Call(cx, funVal, UndefinedHandleValue, args, &dummy);
 }
 
+void PromiseObject::onSettled(JSContext* cx)
+{
+    Rooted<PromiseObject*> promise(cx, this);
+    RootedObject stack(cx);
+    if (!JS::CaptureCurrentStack(cx, &stack, 0)) {
+        cx->clearPendingException();
+        return;
+    }
+    promise->setFixedSlot(PROMISE_RESOLUTION_SITE_SLOT, ObjectOrNullValue(stack));
+    promise->setFixedSlot(PROMISE_RESOLUTION_TIME_SLOT, Now());
+
+    if (promise->state() == JS::PromiseState::Rejected &&
+        promise->getFixedSlot(PROMISE_IS_HANDLED_SLOT).toInt32() !=
+            PROMISE_IS_HANDLED_STATE_HANDLED)
+    {
+        cx->runtime()->addUnhandledRejectedPromise(cx, promise);
+    }
+
+    JS::dbg::onPromiseSettled(cx, promise);
+}
+
 } // namespace js
 
 static JSObject*
 CreatePromisePrototype(JSContext* cx, JSProtoKey key)
 {
     return cx->global()->createBlankPrototype(cx, &PromiseObject::protoClass_);
 }
 
--- a/js/src/builtin/Promise.h
+++ b/js/src/builtin/Promise.h
@@ -14,17 +14,17 @@
 
 namespace js {
 
 class AutoSetNewObjectMetadata;
 
 class PromiseObject : public NativeObject
 {
   public:
-    static const unsigned RESERVED_SLOTS = 11;
+    static const unsigned RESERVED_SLOTS = 12;
     static const Class class_;
     static const Class protoClass_;
     static PromiseObject* create(JSContext* cx, HandleObject executor,
                                  HandleObject proto = nullptr);
 
     JS::PromiseState state() {
         int32_t state = getFixedSlot(PROMISE_STATE_SLOT).toInt32();
         MOZ_ASSERT(state >= 0 && state <= int32_t(JS::PromiseState::Rejected));
@@ -37,27 +37,41 @@ class PromiseObject : public NativeObjec
     Value reason() {
         MOZ_ASSERT(state() == JS::PromiseState::Rejected);
         return getFixedSlot(PROMISE_RESULT_SLOT);
     }
 
     MOZ_MUST_USE bool resolve(JSContext* cx, HandleValue resolutionValue);
     MOZ_MUST_USE bool reject(JSContext* cx, HandleValue rejectionValue);
 
+    void onSettled(JSContext* cx);
+
     double allocationTime() { return getFixedSlot(PROMISE_ALLOCATION_TIME_SLOT).toNumber(); }
     double resolutionTime() { return getFixedSlot(PROMISE_RESOLUTION_TIME_SLOT).toNumber(); }
-    JSObject* allocationSite() { return &getFixedSlot(PROMISE_ALLOCATION_SITE_SLOT).toObject(); }
-    JSObject* resolutionSite() { return &getFixedSlot(PROMISE_RESOLUTION_SITE_SLOT).toObject(); }
+    JSObject* allocationSite() {
+        return getFixedSlot(PROMISE_ALLOCATION_SITE_SLOT).toObjectOrNull();
+    }
+    JSObject* resolutionSite() {
+        return getFixedSlot(PROMISE_RESOLUTION_SITE_SLOT).toObjectOrNull();
+    }
     double lifetime() {
         double now = JS::TimeClip(static_cast<double>(PRMJ_Now()) / PRMJ_USEC_PER_MSEC).toDouble();
         return now - allocationTime();
     }
     double timeToResolution() {
         MOZ_ASSERT(state() != JS::PromiseState::Pending);
         return resolutionTime() - allocationTime();
     }
     MOZ_MUST_USE bool dependentPromises(JSContext* cx, MutableHandle<GCVector<Value>> values);
-    double getID();
+    uint64_t getID();
+    bool markedAsUncaught() {
+        return getFixedSlot(PROMISE_IS_HANDLED_SLOT).toInt32() != PROMISE_IS_HANDLED_STATE_HANDLED;
+    }
+    void markAsReported() {
+        MOZ_ASSERT(getFixedSlot(PROMISE_IS_HANDLED_SLOT).toInt32() ==
+                   PROMISE_IS_HANDLED_STATE_UNHANDLED);
+        setFixedSlot(PROMISE_IS_HANDLED_SLOT, Int32Value(PROMISE_IS_HANDLED_STATE_REPORTED));
+    }
 };
 
 } // namespace js
 
 #endif /* builtin_Promise_h */
--- a/js/src/builtin/Promise.js
+++ b/js/src/builtin/Promise.js
@@ -119,16 +119,17 @@ function CreateResolvingFunctions(promis
 function FulfillPromise(promise, value) {
     return ResolvePromise(promise, value, PROMISE_FULFILL_REACTIONS_SLOT, PROMISE_STATE_FULFILLED);
 }
 function FulfillUnwrappedPromise(value) {
     return ResolvePromise(this, value, PROMISE_FULFILL_REACTIONS_SLOT, PROMISE_STATE_FULFILLED);
 }
 
 // Commoned-out implementation of 25.4.1.4. and 25.4.1.7.
+// ES2016 February 12 draft.
 function ResolvePromise(promise, valueOrReason, reactionsSlot, state) {
     // Step 1.
     assert(GetPromiseState(promise) === PROMISE_STATE_PENDING,
            "Can't resolve non-pending promise");
     assert(state >= PROMISE_STATE_PENDING && state <= PROMISE_STATE_REJECTED,
            `Invalid Promise state <${state}>`);
 
     // Step 2.
@@ -146,22 +147,21 @@ function ResolvePromise(promise, valueOr
     // Step 6.
     UnsafeSetReservedSlot(promise, PROMISE_STATE_SLOT, state);
 
     // Also null out the resolve/reject functions so they can be GC'd.
     UnsafeSetReservedSlot(promise, PROMISE_RESOLVE_FUNCTION_SLOT, null);
     UnsafeSetReservedSlot(promise, PROMISE_REJECT_FUNCTION_SLOT, null);
 
     // Now that everything else is done, do the things the debugger needs.
-    let site = _dbg_captureCurrentStack(0);
-    UnsafeSetReservedSlot(promise, PROMISE_RESOLUTION_SITE_SLOT, site);
-    UnsafeSetReservedSlot(promise, PROMISE_RESOLUTION_TIME_SLOT, std_Date_now());
+    // Step 7 of RejectPromise implemented in the debugger intrinsic.
     _dbg_onPromiseSettled(promise);
 
-    // Step 7.
+    // Step 7 of FulfillPromise.
+    // Step 8 of RejectPromise.
     return TriggerPromiseReactions(reactions, valueOrReason);
 }
 
 // Used to verify that an object is a PromiseCapability record.
 var PromiseCapabilityRecordProto = {__proto__: null};
 
 // ES6, 25.4.1.5.
 // Creates PromiseCapability records, see 25.4.1.1.
@@ -204,29 +204,31 @@ function NewPromiseCapability(C) {
         promise,
         resolve,
         reject
     };
 }
 
 // ES6, 25.4.1.6. is implemented as an intrinsic in SelfHosting.cpp.
 
-// ES6, 25.4.1.7.
+// ES2016, February 12 draft, 25.4.1.7.
 function RejectPromise(promise, reason) {
     return ResolvePromise(promise, reason, PROMISE_REJECT_REACTIONS_SLOT, PROMISE_STATE_REJECTED);
 }
 
 // ES6, 25.4.1.8.
 function TriggerPromiseReactions(reactions, argument) {
     // Step 1.
     for (var i = 0, len = reactions.length; i < len; i++)
         EnqueuePromiseReactionJob(reactions[i], argument);
     // Step 2 (implicit).
 }
 
+// ES2016, February 12 draft 25.4.1.9, implemented in SelfHosting.cpp.
+
 // ES6, 25.4.2.1.
 function EnqueuePromiseReactionJob(reaction, argument) {
     _EnqueuePromiseJob(reaction.capabilities.promise, function PromiseReactionJob() {
         // Step 1.
         assert(IsPromiseReaction(reaction), "Invalid promise reaction record");
 
         // Step 2.
         let promiseCapability = reaction.capabilities;
@@ -896,17 +898,17 @@ function UnwrappedPerformPromiseThen(ful
     function onRejected(argument) {
         return UnsafeCallWrappedFunction(rejectedHandler, undefined, argument);
     }
     return PerformPromiseThen(this, IsCallable(fulfilledHandler) ? onFulfilled : fulfilledHandler,
                               IsCallable(rejectedHandler) ? onRejected : rejectedHandler,
                               resultCapability);
 }
 
-// ES6, 25.4.5.3.1.
+// ES2016, March 1, 2016 draft, 25.4.5.3.1.
 function PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability) {
     // Step 1.
     assert(IsPromise(promise), "Can't call PerformPromiseThen on non-Promise objects");
 
     // Step 2.
     assert(IsPromiseCapability(resultCapability), "Invalid promise capability record");
 
     // Step 3.
@@ -950,25 +952,38 @@ function PerformPromiseThen(promise, onF
         // Step 8.a.
         let value = UnsafeGetReservedSlot(promise, PROMISE_RESULT_SLOT);
 
         // Step 8.b.
         EnqueuePromiseReactionJob(fulfillReaction, value);
     }
 
     // Step 9.
-    else if (state === PROMISE_STATE_REJECTED) {
+    else {
         // Step 9.a.
+        assert(state === PROMISE_STATE_REJECTED, "Invalid Promise state " + state);
+
+        // Step 9.b.
         let reason = UnsafeGetReservedSlot(promise, PROMISE_RESULT_SLOT);
 
-        // Step 9.b.
+        // Step 9.c.
+        if (UnsafeGetInt32FromReservedSlot(promise, PROMISE_IS_HANDLED_SLOT) !==
+            PROMISE_IS_HANDLED_STATE_HANDLED)
+        {
+            HostPromiseRejectionTracker(promise, PROMISE_REJECTION_TRACKER_OPERATION_HANDLE);
+        }
+
+        // Step 9.d.
         EnqueuePromiseReactionJob(rejectReaction, reason);
     }
 
     // Step 10.
+    UnsafeSetReservedSlot(promise, PROMISE_IS_HANDLED_SLOT, PROMISE_IS_HANDLED_STATE_HANDLED);
+
+    // Step 11.
     return resultCapability.promise;
 }
 
 /// Utility functions below.
 function IsPromiseReaction(record) {
     return std_Reflect_getPrototypeOf(record) === PromiseReactionRecordProto;
 }
 
--- a/js/src/builtin/SelfHostingDefines.h
+++ b/js/src/builtin/SelfHostingDefines.h
@@ -63,24 +63,32 @@
 #define PROMISE_REJECT_REACTIONS_SLOT  3
 #define PROMISE_RESOLVE_FUNCTION_SLOT  4
 #define PROMISE_REJECT_FUNCTION_SLOT   5
 #define PROMISE_ALLOCATION_SITE_SLOT   6
 #define PROMISE_RESOLUTION_SITE_SLOT   7
 #define PROMISE_ALLOCATION_TIME_SLOT   8
 #define PROMISE_RESOLUTION_TIME_SLOT   9
 #define PROMISE_ID_SLOT               10
+#define PROMISE_IS_HANDLED_SLOT       11
 
 #define PROMISE_STATE_PENDING   0
 #define PROMISE_STATE_FULFILLED 1
 #define PROMISE_STATE_REJECTED  2
 
+#define PROMISE_IS_HANDLED_STATE_HANDLED   0
+#define PROMISE_IS_HANDLED_STATE_UNHANDLED 1
+#define PROMISE_IS_HANDLED_STATE_REPORTED  2
+
 #define PROMISE_HANDLER_IDENTITY 0
 #define PROMISE_HANDLER_THROWER  1
 
+#define PROMISE_REJECTION_TRACKER_OPERATION_REJECT false
+#define PROMISE_REJECTION_TRACKER_OPERATION_HANDLE true
+
 // NB: keep these in sync with the copy in jsfriendapi.h.
 #define JSITER_OWNONLY    0x8   /* iterate over obj's own properties only */
 #define JSITER_HIDDEN     0x10  /* also enumerate non-enumerable properties */
 #define JSITER_SYMBOLS    0x20  /* also include symbol property keys */
 #define JSITER_SYMBOLSONLY 0x40 /* exclude string property keys */
 
 #define TELEMETRY_DEFINE_GETTER_SETTER_THIS_NULL_UNDEFINED 25
 
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -4772,16 +4772,24 @@ JS_GetInterruptCallback(JSRuntime* rt)
 JS_PUBLIC_API(void)
 JS::SetEnqueuePromiseJobCallback(JSRuntime* rt, JSEnqueuePromiseJobCallback callback,
                                  void* data /* = nullptr */)
 {
     rt->enqueuePromiseJobCallback = callback;
     rt->enqueuePromiseJobCallbackData = data;
 }
 
+extern JS_PUBLIC_API(void)
+JS::SetPromiseRejectionTrackerCallback(JSRuntime* rt, JSPromiseRejectionTrackerCallback callback,
+                                       void* data /* = nullptr */)
+{
+    rt->promiseRejectionTrackerCallback = callback;
+    rt->promiseRejectionTrackerCallbackData = data;
+}
+
 JS_PUBLIC_API(JSObject*)
 JS::NewPromiseObject(JSContext* cx, HandleObject executor, HandleObject proto /* = nullptr */)
 {
     MOZ_ASSERT(!cx->runtime()->isAtomsCompartment(cx->compartment()));
     MOZ_ASSERT(IsCallable(executor));
     AssertHeapIsIdle(cx);
     CHECK_REQUEST(cx);
 
@@ -4813,17 +4821,17 @@ JS::GetPromisePrototype(JSContext* cx)
 
 JS_PUBLIC_API(JS::PromiseState)
 JS::GetPromiseState(JS::HandleObject obj)
 {
     JSObject* promise = CheckedUnwrap(obj);
     return promise->as<PromiseObject>().state();
 }
 
-JS_PUBLIC_API(double)
+JS_PUBLIC_API(uint64_t)
 JS::GetPromiseID(JS::HandleObject promise)
 {
     return promise->as<PromiseObject>().getID();
 }
 
 JS_PUBLIC_API(JS::Value)
 JS::GetPromiseResult(JS::HandleObject promiseObj)
 {
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -601,16 +601,28 @@ typedef void
 
 typedef bool
 (* JSInterruptCallback)(JSContext* cx);
 
 typedef bool
 (* JSEnqueuePromiseJobCallback)(JSContext* cx, JS::HandleObject job,
                                 JS::HandleObject allocationSite, void* data);
 
+enum class PromiseRejectionHandlingState {
+    Unhandled,
+    Handled
+};
+
+typedef void
+(* JSPromiseRejectionTrackerCallback)(JSContext* cx, JS::HandleObject promise,
+                                      PromiseRejectionHandlingState state, void* data);
+
+typedef void
+(* JSProcessPromiseCallback)(JSContext* cx, JS::HandleObject promise);
+
 typedef void
 (* JSErrorReporter)(JSContext* cx, const char* message, JSErrorReport* report);
 
 /**
  * Possible exception types. These types are part of a JSErrorFormatString
  * structure. They define which error to throw in case of a runtime error.
  * JSEXN_NONE marks an unthrowable error.
  */
@@ -4407,16 +4419,25 @@ namespace JS {
  * the corresponding Promise's allocation stack, and the `data` pointer
  * passed here as arguments.
  */
 extern JS_PUBLIC_API(void)
 SetEnqueuePromiseJobCallback(JSRuntime* rt, JSEnqueuePromiseJobCallback callback,
                              void* data = nullptr);
 
 /**
+ * Sets the callback that's invoked whenever a Promise is rejected without
+ * a rejection handler, and when a Promise that was previously rejected
+ * without a handler gets a handler attached.
+ */
+extern JS_PUBLIC_API(void)
+SetPromiseRejectionTrackerCallback(JSRuntime* rt, JSPromiseRejectionTrackerCallback callback,
+                                   void* data = nullptr);
+
+/**
  * Returns a new instance of the Promise builtin class in the current
  * compartment, with the right slot layout. If a `proto` is passed, that gets
  * set as the instance's [[Prototype]] instead of the original value of
  * `Promise.prototype`.
  */
 extern JS_PUBLIC_API(JSObject*)
 NewPromiseObject(JSContext* cx, JS::HandleObject executor, JS::HandleObject proto = nullptr);
 
@@ -4450,17 +4471,17 @@ enum class PromiseState {
  * Returns the given Promise's state as a JS::PromiseState enum value.
  */
 extern JS_PUBLIC_API(PromiseState)
 GetPromiseState(JS::HandleObject promise);
 
 /**
  * Returns the given Promise's process-unique ID.
  */
-JS_PUBLIC_API(double)
+JS_PUBLIC_API(uint64_t)
 GetPromiseID(JS::HandleObject promise);
 
 /**
  * Returns the given Promise's result: either the resolution value for
  * fulfilled promises, or the rejection reason for rejected ones.
  */
 extern JS_PUBLIC_API(JS::Value)
 GetPromiseResult(JS::HandleObject promise);
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -154,16 +154,17 @@ struct ShellRuntime
     bool isWorker;
     double timeoutInterval;
     Atomic<bool> serviceInterrupt;
     Atomic<bool> haveInterruptFunc;
     JS::PersistentRootedValue interruptFunc;
     bool lastWarningEnabled;
     JS::PersistentRootedValue lastWarning;
 #ifdef SPIDERMONKEY_PROMISE
+    JS::PersistentRootedValue promiseRejectionTrackerCallback;
     JS::PersistentRooted<JobQueue> jobQueue;
 #endif // SPIDERMONKEY_PROMISE
 
     /*
      * Watchdog thread state.
      */
     PRLock* watchdogLock;
     PRCondVar* watchdogWakeup;
@@ -306,16 +307,19 @@ extern JS_EXPORT_API(void)   add_history
 ShellRuntime::ShellRuntime(JSRuntime* rt)
   : isWorker(false),
     timeoutInterval(-1.0),
     serviceInterrupt(false),
     haveInterruptFunc(false),
     interruptFunc(rt, NullValue()),
     lastWarningEnabled(false),
     lastWarning(rt, NullValue()),
+#ifdef SPIDERMONKEY_PROMISE
+    promiseRejectionTrackerCallback(rt, NullValue()),
+#endif // SPIDERMONKEY_PROMISE
     watchdogLock(nullptr),
     watchdogWakeup(nullptr),
     watchdogThread(nullptr),
     watchdogHasTimeout(false),
     watchdogTimeout(0),
     sleepWakeup(nullptr),
     exitCode(0),
     quitting(false),
@@ -634,20 +638,22 @@ RunModule(JSContext* cx, const char* fil
 static bool
 ShellEnqueuePromiseJobCallback(JSContext* cx, JS::HandleObject job, JS::HandleObject allocationSite,
                                void* data)
 {
     ShellRuntime* sr = GetShellRuntime(cx);
     MOZ_ASSERT(job);
     return sr->jobQueue.append(job);
 }
+#endif // SPIDERMONKEY_PROMISE
 
 static bool
 DrainJobQueue(JSContext* cx)
 {
+#ifdef SPIDERMONKEY_PROMISE
     ShellRuntime* sr = GetShellRuntime(cx);
     if (sr->quitting)
         return true;
     RootedObject job(cx);
     JS::HandleValueArray args(JS::HandleValueArray::empty());
     RootedValue rval(cx);
     // Execute jobs in a loop until we've reached the end of the queue.
     // Since executing a job can trigger enqueuing of additional jobs,
@@ -655,32 +661,74 @@ DrainJobQueue(JSContext* cx)
     for (size_t i = 0; i < sr->jobQueue.length(); i++) {
         job = sr->jobQueue[i];
         AutoCompartment ac(cx, job);
         if (!JS::Call(cx, UndefinedHandleValue, job, args, &rval))
             JS_ReportPendingException(cx);
         sr->jobQueue[i].set(nullptr);
     }
     sr->jobQueue.clear();
+#endif // SPIDERMONKEY_PROMISE
     return true;
 }
 
 static bool
 DrainJobQueue(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     if (!DrainJobQueue(cx))
         return false;
     args.rval().setUndefined();
     return true;
 }
+
+#ifdef SPIDERMONKEY_PROMISE
+static void
+ForwardingPromiseRejectionTrackerCallback(JSContext* cx, JS::HandleObject promise,
+                                          PromiseRejectionHandlingState state, void* data)
+{
+    RootedValue callback(cx, GetShellRuntime(cx)->promiseRejectionTrackerCallback);
+    if (callback.isNull()) {
+        return;
+    }
+
+    FixedInvokeArgs<2> args(cx);
+    args[0].setObject(*promise);
+    args[1].setInt32(static_cast<int32_t>(state));
+
+    RootedValue rval(cx);
+    if (!Call(cx, callback, UndefinedHandleValue, args, &rval))
+        JS_ClearPendingException(cx);
+}
 #endif // SPIDERMONKEY_PROMISE
 
 static bool
+SetPromiseRejectionTrackerCallback(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+#ifdef SPIDERMONKEY_PROMISE
+    if (!IsCallable(args.get(0))) {
+        JS_ReportError(cx,
+                       "setPromiseRejectionTrackerCallback expects a function as its sole "
+                       "argument");
+        return false;
+    }
+
+    GetShellRuntime(cx)->promiseRejectionTrackerCallback = args[0];
+    JS::SetPromiseRejectionTrackerCallback(cx->runtime(),
+                                           ForwardingPromiseRejectionTrackerCallback);
+
+#endif // SPIDERMONKEY_PROMISE
+    args.rval().setUndefined();
+    return true;
+}
+
+static bool
 EvalAndPrint(JSContext* cx, const char* bytes, size_t length,
              int lineno, bool compileOnly)
 {
     // Eval.
     JS::CompileOptions options(cx);
     options.setIntroductionType("js shell interactive")
            .setUTF8(true)
            .setIsRunOnce(true)
@@ -767,19 +815,17 @@ ReadEvalPrintLoop(JSContext* cx, FILE* i
         if (JS::ForceLexicalInitialization(cx, globalLexical) && gErrFile->isOpen()) {
             fputs("Warning: According to the standard, after the above exception,\n"
                   "Warning: the global bindings should be permanently uninitialized.\n"
                   "Warning: We have non-standard-ly initialized them to `undefined`"
                   "for you.\nWarning: This nicety only happens in the JS shell.\n",
                   stderr);
         }
 
-#ifdef SPIDERMONKEY_PROMISE
         DrainJobQueue(cx);
-#endif // SPIDERMONKEY_PROMISE
 
     } while (!hitEOF && !sr->quitting);
 
     if (gOutFile->isOpen())
         fprintf(gOutFile->fp, "\n");
 }
 
 enum FileKind
@@ -922,17 +968,16 @@ CreateMappedArrayBuffer(JSContext* cx, u
     RootedObject obj(cx, JS_NewMappedArrayBufferWithContents(cx, size, contents));
     if (!obj)
         return false;
 
     args.rval().setObject(*obj);
     return true;
 }
 
-#ifdef SPIDERMONKEY_PROMISE
 static bool
 AddPromiseReactions(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     if (args.length() != 3) {
         JS_ReportErrorNumber(cx, my_GetErrorMessage, nullptr,
                              args.length() < 3 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
@@ -961,17 +1006,16 @@ AddPromiseReactions(JSContext* cx, unsig
     if (!onResolve || !onResolve->is<JSFunction>() || !onReject || !onReject->is<JSFunction>()) {
         JS_ReportErrorNumber(cx, my_GetErrorMessage, nullptr,
                              JSSMSG_INVALID_ARGS, "addPromiseReactions");
         return false;
     }
 
     return JS::AddPromiseReactions(cx, promise, onResolve, onReject);
 }
-#endif // SPIDERMONKEY_PROMISE
 
 static bool
 Options(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     JS::RuntimeOptions oldRuntimeOptions = JS::RuntimeOptionsRef(cx);
     for (unsigned i = 0; i < args.length(); i++) {
@@ -5541,21 +5585,19 @@ static const JSFunctionSpecWithHelp shel
 "         principals of ~0 subsumes all other principals. The absence of a\n"
 "         principal is treated as if its bits were 0xffff, for subsumption\n"
 "         purposes. If this property is omitted, supply no principal."),
 
     JS_FN_HELP("createMappedArrayBuffer", CreateMappedArrayBuffer, 1, 0,
 "createMappedArrayBuffer(filename, [offset, [size]])",
 "  Create an array buffer that mmaps the given file."),
 
-#ifdef SPIDERMONKEY_PROMISE
     JS_FN_HELP("addPromiseReactions", AddPromiseReactions, 3, 0,
 "addPromiseReactions(promise, onResolve, onReject)",
 "  Calls the JS::AddPromiseReactions JSAPI function with the given arguments."),
-#endif // SPIDERMONKEY_PROMISE
 
     JS_FN_HELP("getMaxArgs", GetMaxArgs, 0, 0,
 "getMaxArgs()",
 "  Return the maximum number of supported args for a call."),
 
     JS_FN_HELP("objectEmulatingUndefined", ObjectEmulatingUndefined, 0, 0,
 "objectEmulatingUndefined()",
 "  Return a new object obj for which typeof obj === \"undefined\", obj == null\n"
@@ -5620,22 +5662,25 @@ static const JSFunctionSpecWithHelp shel
 "{ eval }: Apply JS::Evaluate to |params.eval|.\n"
 "\n"
 "The return value is an array of strings, with one element for each\n"
 "JavaScript invocation that occurred as a result of the given\n"
 "operation. Each element is the name of the function invoked, or the\n"
 "string 'eval:FILENAME' if the code was invoked by 'eval' or something\n"
 "similar.\n"),
 
-#ifdef SPIDERMONKEY_PROMISE
     JS_FN_HELP("drainJobQueue", DrainJobQueue, 0, 0,
 "drainJobQueue()",
 "Take jobs from the shell's job queue in FIFO order and run them until the\n"
 "queue is empty.\n"),
-#endif // SPIDERMONKEY_PROMISE
+
+    JS_FN_HELP("setPromiseRejectionTrackerCallback", SetPromiseRejectionTrackerCallback, 1, 0,
+"setPromiseRejectionTrackerCallback()",
+"Sets the callback to be invoked whenever a Promise rejection is unhandled\n"
+"or a previously-unhandled rejection becomes handled."),
 
     JS_FS_HELP_END
 };
 
 static const JSFunctionSpecWithHelp fuzzing_unsafe_functions[] = {
     JS_FN_HELP("clone", Clone, 1, 0,
 "clone(fun[, scope])",
 "  Clone function object."),
@@ -6736,19 +6781,17 @@ ProcessArgs(JSContext* cx, OptionParser*
 
     /* The |script| argument is processed after all options. */
     if (const char* path = op->getStringArg("script")) {
         Process(cx, path, false);
         if (sr->exitCode)
             return sr->exitCode;
     }
 
-#ifdef SPIDERMONKEY_PROMISE
     DrainJobQueue(cx);
-#endif // SPIDERMONKEY_PROMISE
 
     if (op->getBoolOption('i'))
         Process(cx, nullptr, true);
 
     return sr->exitCode ? sr->exitCode : EXIT_SUCCESS;
 }
 
 static bool
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Promise/promise-rejection-tracking.js
@@ -0,0 +1,36 @@
+// |reftest| skip-if(!xulRuntime.shell) -- needs setPromiseRejectionTrackerCallback
+
+if (!this.Promise) {
+    this.reportCompare && reportCompare(true,true);
+    quit(0);
+}
+
+const UNHANDLED = 0;
+const HANDLED   = 1;
+
+let rejections = new Map();
+function rejectionTracker(promise, state) {
+    rejections.set(promise, state);
+}
+setPromiseRejectionTrackerCallback(rejectionTracker);
+
+// Unhandled rejections are tracked.
+let reject;
+let p = new Promise((res_, rej_) => (reject = rej_));
+assertEq(rejections.has(p), false);
+reject('reason');
+assertEq(rejections.get(p), UNHANDLED);
+// Later handling updates the tracking.
+p.then(_=>_, _=>_);
+assertEq(rejections.get(p), HANDLED);
+
+rejections.clear();
+
+// Handled rejections aren't tracked at all.
+p = new Promise((res_, rej_) => (reject = rej_));
+assertEq(rejections.has(p), false);
+p.then(_=>_, _=>_);
+reject('reason');
+assertEq(rejections.has(p), false);
+
+this.reportCompare && reportCompare(true,true);
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -8156,17 +8156,17 @@ DebuggerObject_getPromiseResolutionSite(
     return true;
 }
 
 static bool
 DebuggerObject_getPromiseID(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, "get promiseID", args, refobj);
 
-    args.rval().setNumber(promise->getID());
+    args.rval().setNumber(double(promise->getID()));
     return true;
 }
 
 static bool
 DebuggerObject_getPromiseDependentPromises(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGOBJECT_OWNER_PROMISE(cx, argc, vp, "get promiseDependentPromises", args, dbg, refobj);
 
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -36,16 +36,17 @@
 #include "jsnativestack.h"
 #include "jsobj.h"
 #include "jsscript.h"
 #include "jswatchpoint.h"
 #include "jswin.h"
 #include "jswrapper.h"
 
 #include "asmjs/WasmSignalHandlers.h"
+#include "builtin/Promise.h"
 #include "jit/arm/Simulator-arm.h"
 #include "jit/arm64/vixl/Simulator-vixl.h"
 #include "jit/JitCompartment.h"
 #include "jit/mips32/Simulator-mips32.h"
 #include "jit/mips64/Simulator-mips64.h"
 #include "jit/PcScriptCache.h"
 #include "js/Date.h"
 #include "js/MemoryMetrics.h"
@@ -152,16 +153,18 @@ JSRuntime::JSRuntime(JSRuntime* parentRu
 #endif
     interrupt_(false),
     telemetryCallback(nullptr),
     handlingSegFault(false),
     handlingJitInterrupt_(false),
     interruptCallback(nullptr),
     enqueuePromiseJobCallback(nullptr),
     enqueuePromiseJobCallbackData(nullptr),
+    promiseRejectionTrackerCallback(nullptr),
+    promiseRejectionTrackerCallbackData(nullptr),
 #ifdef DEBUG
     exclusiveAccessOwner(nullptr),
     mainThreadHasExclusiveAccess(false),
 #endif
     numExclusiveThreads(0),
     numCompartments(0),
     localeCallbacks(nullptr),
     defaultLocale(nullptr),
@@ -782,16 +785,40 @@ JSRuntime::enqueuePromiseJob(JSContext* 
     void* data = cx->runtime()->enqueuePromiseJobCallbackData;
     RootedObject allocationSite(cx);
     if (promise)
         allocationSite = JS::GetPromiseAllocationSite(promise);
     return cx->runtime()->enqueuePromiseJobCallback(cx, job, allocationSite, data);
 }
 
 void
+JSRuntime::addUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise)
+{
+    MOZ_ASSERT(promise->is<PromiseObject>());
+    if (!cx->runtime()->promiseRejectionTrackerCallback)
+        return;
+
+    void* data = cx->runtime()->promiseRejectionTrackerCallbackData;
+    cx->runtime()->promiseRejectionTrackerCallback(cx, promise,
+                                                   PromiseRejectionHandlingState::Unhandled, data);
+}
+
+void
+JSRuntime::removeUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise)
+{
+    MOZ_ASSERT(promise->is<PromiseObject>());
+    if (!cx->runtime()->promiseRejectionTrackerCallback)
+        return;
+
+    void* data = cx->runtime()->promiseRejectionTrackerCallbackData;
+    cx->runtime()->promiseRejectionTrackerCallback(cx, promise,
+                                                   PromiseRejectionHandlingState::Handled, data);
+}
+
+void
 JSRuntime::updateMallocCounter(size_t nbytes)
 {
     updateMallocCounter(nullptr, nbytes);
 }
 
 void
 JSRuntime::updateMallocCounter(JS::Zone* zone, size_t nbytes)
 {
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -932,16 +932,19 @@ struct JSRuntime : public JS::shadow::Ru
         return handlingJitInterrupt_;
     }
 
     JSInterruptCallback interruptCallback;
 
     JSEnqueuePromiseJobCallback enqueuePromiseJobCallback;
     void* enqueuePromiseJobCallbackData;
 
+    JSPromiseRejectionTrackerCallback promiseRejectionTrackerCallback;
+    void* promiseRejectionTrackerCallbackData;
+
 #ifdef DEBUG
     void assertCanLock(js::RuntimeLock which);
 #else
     void assertCanLock(js::RuntimeLock which) {}
 #endif
 
   private:
     /*
@@ -1041,16 +1044,18 @@ struct JSRuntime : public JS::shadow::Ru
     bool hasJitRuntime() const {
         return !!jitRuntime_;
     }
     js::InterpreterStack& interpreterStack() {
         return interpreterStack_;
     }
 
     bool enqueuePromiseJob(JSContext* cx, js::HandleFunction job, js::HandleObject promise);
+    void addUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
+    void removeUnhandledRejectedPromise(JSContext* cx, js::HandleObject promise);
 
     //-------------------------------------------------------------------------
     // Self-hosting support
     //-------------------------------------------------------------------------
 
     bool initSelfHosting(JSContext* cx);
     void finishSelfHosting();
     void markSelfHostingGlobal(JSTracer* trc);
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -1795,16 +1795,32 @@ intrinsic_EnqueuePromiseJob(JSContext* c
     RootedObject promise(cx, &args[0].toObject());
     RootedFunction job(cx, &args[1].toObject().as<JSFunction>());
     if (!cx->runtime()->enqueuePromiseJob(cx, job, promise))
         return false;
     args.rval().setUndefined();
     return true;
 }
 
+// ES2016, February 12 draft, 25.4.1.9.
+static bool
+intrinsic_HostPromiseRejectionTracker(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    MOZ_ASSERT(args.length() == 2);
+    MOZ_ASSERT(args[0].toObject().is<PromiseObject>());
+
+    Rooted<PromiseObject*> promise(cx, &args[0].toObject().as<PromiseObject>());
+    mozilla::DebugOnly<bool> isHandled = args[1].toBoolean();
+    MOZ_ASSERT(isHandled, "HostPromiseRejectionTracker intrinsic currently only marks as handled");
+    cx->runtime()->removeUnhandledRejectedPromise(cx, promise);
+    args.rval().setUndefined();
+    return true;
+}
+
 /**
  * Returns the default locale as a well-formed, but not necessarily canonicalized,
  * BCP-47 language tag.
  */
 static bool
 intrinsic_RuntimeDefaultLocale(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
@@ -2153,46 +2169,22 @@ intrinsic_ModuleNamespaceExports(JSConte
  * resulting state has been set on the promise, and it's up to the debugger
  * to act on this signal in whichever way it wants.
  */
 static bool
 intrinsic_onPromiseSettled(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     MOZ_ASSERT(args.length() == 1);
-    RootedObject promise(cx, &args[0].toObject());
-    JS::dbg::onPromiseSettled(cx, promise);
+    Rooted<PromiseObject*> promise(cx, &args[0].toObject().as<PromiseObject>());
+    promise->onSettled(cx);
     args.rval().setUndefined();
     return true;
 }
 
-/**
- * Intrinsic used to tell the debugger about settled promises.
- *
- * This is invoked both when resolving and rejecting promises, after the
- * resulting state has been set on the promise, and it's up to the debugger
- * to act on this signal in whichever way it wants.
- */
-static bool
-intrinsic_captureCurrentStack(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    MOZ_ASSERT(args.length() < 2);
-    unsigned maxFrameCount = 0;
-    if (args.length() == 1)
-        maxFrameCount = args[0].toInt32();
-
-    RootedObject stack(cx);
-    if (!JS::CaptureCurrentStack(cx, &stack, maxFrameCount))
-        return false;
-
-    args.rval().setObject(*stack);
-    return true;
-}
-
 // The self-hosting global isn't initialized with the normal set of builtins.
 // Instead, individual C++-implemented functions that're required by
 // self-hosted code are defined as global functions. Accessing these
 // functions via a content compartment's builtins would be unsafe, because
 // content script might have changed the builtins' prototypes' members.
 // Installing the whole set of builtins in the self-hosting compartment, OTOH,
 // would be wasteful: it increases memory usage and initialization time for
 // self-hosting compartment.
@@ -2444,16 +2436,17 @@ static const JSFunctionSpec intrinsic_fu
 
     JS_FN("IsWeakSet", intrinsic_IsInstanceOfBuiltin<WeakSetObject>, 1,0),
     JS_FN("CallWeakSetMethodIfWrapped",
           CallNonGenericSelfhostedMethod<Is<WeakSetObject>>, 2, 0),
 
     JS_FN("IsPromise",                      intrinsic_IsInstanceOfBuiltin<PromiseObject>, 1,0),
     JS_FN("IsWrappedPromise",               intrinsic_IsWrappedPromiseObject,     1, 0),
     JS_FN("_EnqueuePromiseJob",             intrinsic_EnqueuePromiseJob,          1, 0),
+    JS_FN("HostPromiseRejectionTracker",    intrinsic_HostPromiseRejectionTracker,2, 0),
     JS_FN("_GetOriginalPromiseConstructor", intrinsic_OriginalPromiseConstructor, 0, 0),
     JS_FN("RejectUnwrappedPromise",         intrinsic_RejectUnwrappedPromise,     2, 0),
     JS_FN("CallPromiseMethodIfWrapped",
           CallNonGenericSelfhostedMethod<Is<PromiseObject>>,      2,0),
 
     // See builtin/TypedObject.h for descriptors of the typedobj functions.
     JS_FN("NewOpaqueTypedObject",           js::NewOpaqueTypedObject, 1, 0),
     JS_FN("NewDerivedTypedObject",          js::NewDerivedTypedObject, 3, 0),
@@ -2556,17 +2549,16 @@ static const JSFunctionSpec intrinsic_fu
     JS_FN("SetModuleEvaluated", intrinsic_SetModuleEvaluated, 1, 0),
     JS_FN("EvaluateModule", intrinsic_EvaluateModule, 1, 0),
     JS_FN("IsModuleNamespace", intrinsic_IsInstanceOfBuiltin<ModuleNamespaceObject>, 1, 0),
     JS_FN("NewModuleNamespace", intrinsic_NewModuleNamespace, 2, 0),
     JS_FN("AddModuleNamespaceBinding", intrinsic_AddModuleNamespaceBinding, 4, 0),
     JS_FN("ModuleNamespaceExports", intrinsic_ModuleNamespaceExports, 1, 0),
 
     JS_FN("_dbg_onPromiseSettled", intrinsic_onPromiseSettled, 1, 0),
-    JS_FN("_dbg_captureCurrentStack", intrinsic_captureCurrentStack, 1, 0),
 
     JS_FS_END
 };
 
 void
 js::FillSelfHostingCompileOptions(CompileOptions& options)
 {
     /*