Bug 1083361 - Exposing a PromiseDebugging API to monitor uncaught DOM Promise. r=bz
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Fri, 10 Apr 2015 17:27:57 +0200
changeset 238965 331d71cabe1ef6ec803d3e6e8bb85b44d702a187
parent 238964 33e89c9a41728ecb37f8d6052858bf327d46a5cf
child 238966 72c7745a331ae2bc88f072cc527cea556ff71083
push id58385
push usercbook@mozilla.com
push dateTue, 14 Apr 2015 12:09:41 +0000
treeherdermozilla-inbound@8d1e4bfdbb9a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs1083361
milestone40.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 1083361 - Exposing a PromiseDebugging API to monitor uncaught DOM Promise. r=bz
dom/promise/Promise.cpp
dom/promise/Promise.h
dom/promise/PromiseDebugging.cpp
dom/promise/PromiseDebugging.h
dom/promise/moz.build
dom/promise/tests/browser.ini
dom/promise/tests/browser_monitorUncaught.js
dom/webidl/PromiseDebugging.webidl
dom/workers/WorkerPrivate.cpp
layout/build/nsLayoutStatics.cpp
xpcom/base/CycleCollectedJSRuntime.cpp
xpcom/base/CycleCollectedJSRuntime.h
--- a/dom/promise/Promise.cpp
+++ b/dom/promise/Promise.cpp
@@ -9,35 +9,42 @@
 #include "jsfriendapi.h"
 #include "js/Debug.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/DOMError.h"
 #include "mozilla/dom/OwningNonNull.h"
 #include "mozilla/dom/PromiseBinding.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/MediaStreamError.h"
+#include "mozilla/Atomics.h"
 #include "mozilla/CycleCollectedJSRuntime.h"
 #include "mozilla/Preferences.h"
 #include "PromiseCallback.h"
+#include "PromiseDebugging.h"
 #include "PromiseNativeHandler.h"
 #include "PromiseWorkerProxy.h"
 #include "nsContentUtils.h"
 #include "WorkerPrivate.h"
 #include "WorkerRunnable.h"
 #include "nsJSPrincipals.h"
 #include "nsJSUtils.h"
 #include "nsPIDOMWindow.h"
 #include "nsJSEnvironment.h"
 #include "nsIScriptObjectPrincipal.h"
 #include "xpcpublic.h"
 #include "nsGlobalWindow.h"
 
 namespace mozilla {
 namespace dom {
 
+namespace {
+// Generator used by Promise::GetID.
+Atomic<uintptr_t> gIDGenerator(0);
+}
+
 using namespace workers;
 
 NS_IMPL_ISUPPORTS0(PromiseNativeHandler)
 
 // This class processes the promise's callbacks with promise's result.
 class PromiseCallbackTask final : public nsRunnable
 {
 public:
@@ -264,17 +271,21 @@ private:
   NS_DECL_OWNINGTHREAD;
 };
 
 // Promise
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(Promise)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise)
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
   tmp->MaybeReportRejectedOnce();
+#else
+  tmp->mResult = JS::UndefinedValue();
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mResolveCallbacks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mRejectCallbacks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Promise)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
@@ -328,29 +339,37 @@ NS_INTERFACE_MAP_END
 
 Promise::Promise(nsIGlobalObject* aGlobal)
   : mGlobal(aGlobal)
   , mResult(JS::UndefinedValue())
   , mAllocationStack(nullptr)
   , mRejectionStack(nullptr)
   , mFullfillmentStack(nullptr)
   , mState(Pending)
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
   , mHadRejectCallback(false)
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
+  , mTaskPending(false)
   , mResolvePending(false)
+  , mIsLastInChain(true)
+  , mWasNotifiedAsUncaught(false)
+  , mID(0)
 {
   MOZ_ASSERT(mGlobal);
 
   mozilla::HoldJSObjects(this);
 
   mCreationTimestamp = TimeStamp::Now();
 }
 
 Promise::~Promise()
 {
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
   MaybeReportRejectedOnce();
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
   mozilla::DropJSObjects(this);
 }
 
 JSObject*
 Promise::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return PromiseBinding::Wrap(aCx, this, aGivenProto);
 }
@@ -1003,49 +1022,58 @@ Promise::Compartment() const
 {
   return js::GetObjectCompartment(GlobalJSObject());
 }
 
 void
 Promise::AppendCallbacks(PromiseCallback* aResolveCallback,
                          PromiseCallback* aRejectCallback)
 {
-  if (aResolveCallback) {
-    mResolveCallbacks.AppendElement(aResolveCallback);
-  }
+  MOZ_ASSERT(aResolveCallback);
+  MOZ_ASSERT(aRejectCallback);
 
-  if (aRejectCallback) {
-    mHadRejectCallback = true;
-    mRejectCallbacks.AppendElement(aRejectCallback);
+  if (mIsLastInChain && mState == PromiseState::Rejected) {
+    // This rejection is now consumed.
+    PromiseDebugging::AddConsumedRejection(*this);
+    // Note that we may not have had the opportunity to call
+    // RunResolveTask() yet, so we may never have called
+    // `PromiseDebugging:AddUncaughtRejection`.
+  }
+  mIsLastInChain = false;
 
-    // Now that there is a callback, we don't need to report anymore.
-    RemoveFeature();
-  }
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
+  // Now that there is a callback, we don't need to report anymore.
+  mHadRejectCallback = true;
+  RemoveFeature();
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
+
+  mResolveCallbacks.AppendElement(aResolveCallback);
+  mRejectCallbacks.AppendElement(aRejectCallback);
 
   // If promise's state is fulfilled, queue a task to process our fulfill
   // callbacks with promise's result. If promise's state is rejected, queue a
   // task to process our reject callbacks with promise's result.
   if (mState != Pending) {
     EnqueueCallbackTasks();
   }
 }
 
 class WrappedWorkerRunnable final : public WorkerSameThreadRunnable
 {
 public:
-  WrappedWorkerRunnable(WorkerPrivate* aWorkerPrivate, nsIRunnable* aRunnable)
+  WrappedWorkerRunnable(workers::WorkerPrivate* aWorkerPrivate, nsIRunnable* aRunnable)
     : WorkerSameThreadRunnable(aWorkerPrivate)
     , mRunnable(aRunnable)
   {
     MOZ_ASSERT(aRunnable);
     MOZ_COUNT_CTOR(WrappedWorkerRunnable);
   }
 
   bool
-  WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
+  WorkerRun(JSContext* aCx, workers::WorkerPrivate* aWorkerPrivate) override
   {
     NS_ASSERT_OWNINGTHREAD(WrappedWorkerRunnable);
     mRunnable->Run();
     return true;
   }
 
 private:
   virtual
@@ -1066,16 +1094,17 @@ Promise::DispatchToMicroTask(nsIRunnable
 
   CycleCollectedJSRuntime* runtime = CycleCollectedJSRuntime::Get();
   nsTArray<nsCOMPtr<nsIRunnable>>& microtaskQueue =
     runtime->GetPromiseMicroTaskQueue();
 
   microtaskQueue.AppendElement(aRunnable);
 }
 
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
 void
 Promise::MaybeReportRejected()
 {
   if (mState != Rejected || mHadRejectCallback || mResult.isUndefined()) {
     return;
   }
 
   AutoJSAPI jsapi;
@@ -1110,16 +1139,17 @@ Promise::MaybeReportRejected()
   // Now post an event to do the real reporting async
   // Since Promises preserve their wrapper, it is essential to nsRefPtr<> the
   // AsyncErrorReporter, otherwise if the call to DispatchToMainThread fails, it
   // will leak. See Bug 958684.
   nsRefPtr<AsyncErrorReporter> r =
     new AsyncErrorReporter(CycleCollectedJSRuntime::Get()->Runtime(), xpcReport);
   NS_DispatchToMainThread(r);
 }
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
 
 void
 Promise::MaybeResolveInternal(JSContext* aCx,
                               JS::Handle<JS::Value> aValue)
 {
   if (mResolvePending) {
     return;
   }
@@ -1199,34 +1229,45 @@ Promise::Settle(JS::Handle<JS::Value> aV
   AutoJSAPI jsapi;
   jsapi.Init();
   JSContext* cx = jsapi.cx();
   JS::RootedObject wrapper(cx, GetWrapper());
   MOZ_ASSERT(wrapper); // We preserved it
   JSAutoCompartment ac(cx, wrapper);
   JS::dbg::onPromiseSettled(cx, wrapper);
 
+  if (aState == PromiseState::Rejected &&
+      mIsLastInChain) {
+    // The Promise has just been rejected, and it is last in chain.
+    // We need to inform PromiseDebugging.
+    // If the Promise is eventually not the last in chain anymore,
+    // we will need to inform PromiseDebugging again.
+    PromiseDebugging::AddUncaughtRejection(*this);
+  }
+
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
   // If the Promise was rejected, and there is no reject handler already setup,
   // watch for thread shutdown.
   if (aState == PromiseState::Rejected &&
       !mHadRejectCallback &&
       !NS_IsMainThread()) {
-    WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+    workers::WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
     MOZ_ASSERT(worker);
     worker->AssertIsOnWorkerThread();
 
     mFeature = new PromiseReportRejectFeature(this);
     if (NS_WARN_IF(!worker->AddFeature(worker->GetJSContext(), mFeature))) {
       // To avoid a false RemoveFeature().
       mFeature = nullptr;
       // Worker is shutting down, report rejection immediately since it is
       // unlikely that reject callbacks will be added after this point.
       MaybeReportRejectedOnce();
     }
   }
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
 
   EnqueueCallbackTasks();
 }
 
 void
 Promise::MaybeSettle(JS::Handle<JS::Value> aValue,
                      PromiseState aState)
 {
@@ -1251,35 +1292,37 @@ Promise::EnqueueCallbackTasks()
 
   for (uint32_t i = 0; i < callbacks.Length(); ++i) {
     nsRefPtr<PromiseCallbackTask> task =
       new PromiseCallbackTask(this, callbacks[i], mResult);
     DispatchToMicroTask(task);
   }
 }
 
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
 void
 Promise::RemoveFeature()
 {
   if (mFeature) {
-    WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+    workers::WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
     MOZ_ASSERT(worker);
     worker->RemoveFeature(worker->GetJSContext(), mFeature);
     mFeature = nullptr;
   }
 }
 
 bool
 PromiseReportRejectFeature::Notify(JSContext* aCx, workers::Status aStatus)
 {
   MOZ_ASSERT(aStatus > workers::Running);
   mPromise->MaybeReportRejectedOnce();
   // After this point, `this` has been deleted by RemoveFeature!
   return true;
 }
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
 
 bool
 Promise::CaptureStack(JSContext* aCx, JS::Heap<JSObject*>& aTarget)
 {
   JS::Rooted<JSObject*> stack(aCx);
   if (!JS::CaptureCurrentStack(aCx, &stack)) {
     return false;
   }
@@ -1369,17 +1412,17 @@ private:
   JSAutoStructuredCloneBuffer mBuffer;
 
   // Function pointer for calling Promise::{ResolveInternal,RejectInternal}.
   PromiseWorkerProxy::RunCallbackFunc mFunc;
 };
 
 /* static */
 already_AddRefed<PromiseWorkerProxy>
-PromiseWorkerProxy::Create(WorkerPrivate* aWorkerPrivate,
+PromiseWorkerProxy::Create(workers::WorkerPrivate* aWorkerPrivate,
                            Promise* aWorkerPromise,
                            const JSStructuredCloneCallbacks* aCb)
 {
   MOZ_ASSERT(aWorkerPrivate);
   aWorkerPrivate->AssertIsOnWorkerThread();
   MOZ_ASSERT(aWorkerPromise);
 
   nsRefPtr<PromiseWorkerProxy> proxy =
@@ -1393,34 +1436,34 @@ PromiseWorkerProxy::Create(WorkerPrivate
     proxy->mCleanedUp = true;
     proxy->mWorkerPromise = nullptr;
     return nullptr;
   }
 
   return proxy.forget();
 }
 
-PromiseWorkerProxy::PromiseWorkerProxy(WorkerPrivate* aWorkerPrivate,
+PromiseWorkerProxy::PromiseWorkerProxy(workers::WorkerPrivate* aWorkerPrivate,
                                        Promise* aWorkerPromise,
                                        const JSStructuredCloneCallbacks* aCallbacks)
   : mWorkerPrivate(aWorkerPrivate)
   , mWorkerPromise(aWorkerPromise)
   , mCleanedUp(false)
   , mCallbacks(aCallbacks)
   , mCleanUpLock("cleanUpLock")
 {
 }
 
 PromiseWorkerProxy::~PromiseWorkerProxy()
 {
   MOZ_ASSERT(mCleanedUp);
   MOZ_ASSERT(!mWorkerPromise);
 }
 
-WorkerPrivate*
+workers::WorkerPrivate*
 PromiseWorkerProxy::GetWorkerPrivate() const
 {
   // It's ok to race on |mCleanedUp|, because it will never cause us to fire
   // the assertion when we should not.
   MOZ_ASSERT(!mCleanedUp);
 
   return mWorkerPrivate;
 }
@@ -1438,17 +1481,17 @@ PromiseWorkerProxy::StoreISupports(nsISu
 
   nsMainThreadPtrHandle<nsISupports> supports(
     new nsMainThreadPtrHolder<nsISupports>(aSupports));
   mSupportsArray.AppendElement(supports);
 }
 
 bool
 PromiseWorkerProxyControlRunnable::WorkerRun(JSContext* aCx,
-                                             WorkerPrivate* aWorkerPrivate)
+                                             workers::WorkerPrivate* aWorkerPrivate)
 {
   mProxy->CleanUp(aCx);
   return true;
 }
 
 void
 PromiseWorkerProxy::RunCallback(JSContext* aCx,
                                 JS::Handle<JS::Value> aValue,
@@ -1540,10 +1583,18 @@ template<>
 void Promise::MaybeRejectBrokenly(const nsRefPtr<DOMError>& aArg) {
   MaybeSomething(aArg, &Promise::MaybeReject);
 }
 template<>
 void Promise::MaybeRejectBrokenly(const nsAString& aArg) {
   MaybeSomething(aArg, &Promise::MaybeReject);
 }
 
+uint64_t
+Promise::GetID() {
+  if (mID != 0) {
+    return mID;
+  }
+  return mID = ++gIDGenerator;
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/promise/Promise.h
+++ b/dom/promise/Promise.h
@@ -16,61 +16,75 @@
 #include "mozilla/dom/PromiseBinding.h"
 #include "mozilla/dom/ToJSValue.h"
 #include "mozilla/WeakPtr.h"
 #include "nsWrapperCache.h"
 #include "nsAutoPtr.h"
 #include "js/TypeDecls.h"
 #include "jspubtd.h"
 
+// Bug 1083361 introduces a new mechanism for tracking uncaught
+// rejections. This #define serves to track down the parts of code
+// that need to be removed once clients have been put together
+// to take advantage of the new mechanism. New code should not
+// depend on code #ifdefed to this #define.
+#define DOM_PROMISE_DEPRECATED_REPORTING 1
+
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
 #include "mozilla/dom/workers/bindings/WorkerFeature.h"
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
 
 class nsIGlobalObject;
 
 namespace mozilla {
 namespace dom {
 
 class AnyCallback;
 class DOMError;
 class MediaStreamError;
 class PromiseCallback;
 class PromiseInit;
 class PromiseNativeHandler;
 class PromiseDebugging;
 
 class Promise;
+
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
 class PromiseReportRejectFeature : public workers::WorkerFeature
 {
   // The Promise that owns this feature.
   Promise* mPromise;
 
 public:
   explicit PromiseReportRejectFeature(Promise* aPromise)
     : mPromise(aPromise)
   {
     MOZ_ASSERT(mPromise);
   }
 
   virtual bool
   Notify(JSContext* aCx, workers::Status aStatus) override;
 };
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
 
 #define NS_PROMISE_IID \
   { 0x1b8d6215, 0x3e67, 0x43ba, \
     { 0x8a, 0xf9, 0x31, 0x5e, 0x8f, 0xce, 0x75, 0x65 } }
 
 class Promise : public nsISupports,
                 public nsWrapperCache,
                 public SupportsWeakPtr<Promise>
 {
   friend class NativePromiseCallback;
   friend class PromiseCallbackTask;
   friend class PromiseResolverTask;
   friend class PromiseTask;
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
   friend class PromiseReportRejectFeature;
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
   friend class PromiseWorkerProxy;
   friend class PromiseWorkerProxyRunnable;
   friend class RejectPromiseCallback;
   friend class ResolvePromiseCallback;
   friend class ThenableResolverTask;
   friend class WrapperPromiseCallback;
 
 public:
@@ -177,16 +191,19 @@ public:
        const Sequence<JS::Value>& aIterable, ErrorResult& aRv);
 
   void AppendNativeHandler(PromiseNativeHandler* aRunnable);
 
   JSObject* GlobalJSObject() const;
 
   JSCompartment* Compartment() const;
 
+  // Return a unique-to-the-process identifier for this Promise.
+  uint64_t GetID();
+
 protected:
   // Do NOT call this unless you're Promise::Create.  I wish we could enforce
   // that from inside this class too, somehow.
   explicit Promise(nsIGlobalObject* aGlobal);
 
   virtual ~Promise();
 
   // Queue an async microtask to current main or worker thread.
@@ -204,16 +221,31 @@ protected:
 
   bool IsPending()
   {
     return mResolvePending;
   }
 
   void GetDependentPromises(nsTArray<nsRefPtr<Promise>>& aPromises);
 
+  bool IsLastInChain() const
+  {
+    return mIsLastInChain;
+  }
+
+  void SetNotifiedAsUncaught()
+  {
+    mWasNotifiedAsUncaught = true;
+  }
+
+  bool WasNotifiedAsUncaught() const
+  {
+    return mWasNotifiedAsUncaught;
+  }
+
 private:
   friend class PromiseDebugging;
 
   enum PromiseState {
     Pending,
     Resolved,
     Rejected
   };
@@ -237,26 +269,28 @@ private:
   void EnqueueCallbackTasks();
 
   void Settle(JS::Handle<JS::Value> aValue, Promise::PromiseState aState);
   void MaybeSettle(JS::Handle<JS::Value> aValue, Promise::PromiseState aState);
 
   void AppendCallbacks(PromiseCallback* aResolveCallback,
                        PromiseCallback* aRejectCallback);
 
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
   // If we have been rejected and our mResult is a JS exception,
   // report it to the error console.
   // Use MaybeReportRejectedOnce() for actual calls.
   void MaybeReportRejected();
 
   void MaybeReportRejectedOnce() {
     MaybeReportRejected();
     RemoveFeature();
     mResult.setUndefined();
   }
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
 
   void MaybeResolveInternal(JSContext* aCx,
                             JS::Handle<JS::Value> aValue);
   void MaybeRejectInternal(JSContext* aCx,
                            JS::Handle<JS::Value> aValue);
 
   void ResolveInternal(JSContext* aCx,
                        JS::Handle<JS::Value> aValue);
@@ -294,17 +328,19 @@ private:
   static JSObject*
   CreateFunction(JSContext* aCx, Promise* aPromise, int32_t aTask);
 
   static JSObject*
   CreateThenableFunction(JSContext* aCx, Promise* aPromise, uint32_t aTask);
 
   void HandleException(JSContext* aCx);
 
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
   void RemoveFeature();
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
 
   // Capture the current stack and store it in aTarget.  If false is
   // returned, an exception is presumably pending on aCx.
   bool CaptureStack(JSContext* aCx, JS::Heap<JSObject*>& aTarget);
 
   nsRefPtr<nsIGlobalObject> mGlobal;
 
   nsTArray<nsRefPtr<PromiseCallback> > mResolveCallbacks;
@@ -320,31 +356,48 @@ private:
   // have a rejection stack.
   JS::Heap<JSObject*> mRejectionStack;
   // mFullfillmentStack is only set when the promise is fulfilled directly from
   // script, by calling Promise.resolve() or the fulfillment callback we pass to
   // the PromiseInit function.  Promises that are fulfilled internally do not
   // have a fulfillment stack.
   JS::Heap<JSObject*> mFullfillmentStack;
   PromiseState mState;
+
+#if defined(DOM_PROMISE_DEPRECATED_REPORTING)
   bool mHadRejectCallback;
 
-  bool mResolvePending;
-
   // If a rejected promise on a worker has no reject callbacks attached, it
   // needs to know when the worker is shutting down, to report the error on the
   // console before the worker's context is deleted. This feature is used for
   // that purpose.
   nsAutoPtr<PromiseReportRejectFeature> mFeature;
+#endif // defined(DOM_PROMISE_DEPRECATED_REPORTING)
+
+  bool mTaskPending;
+  bool mResolvePending;
+
+  // `true` if this Promise is the last in the chain, or `false` if
+  // another Promise has been created from this one by a call to
+  // `then`, `all`, `race`, etc.
+  bool mIsLastInChain;
+
+  // `true` if PromiseDebugging has already notified at least one observer that
+  // this promise was left uncaught, `false` otherwise.
+  bool mWasNotifiedAsUncaught;
 
   // The time when this promise was created.
   TimeStamp mCreationTimestamp;
 
   // The time when this promise transitioned out of the pending state.
   TimeStamp mSettlementTimestamp;
+
+  // Once `GetID()` has been called, a unique-to-the-process identifier for this
+  // promise. Until then, `0`.
+  uint64_t mID;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(Promise, NS_PROMISE_IID)
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_Promise_h
--- a/dom/promise/PromiseDebugging.cpp
+++ b/dom/promise/PromiseDebugging.cpp
@@ -1,26 +1,75 @@
 /* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#include "mozilla/dom/PromiseDebugging.h"
-
 #include "js/Value.h"
+#include "nsThreadUtils.h"
 
+#include "mozilla/CycleCollectedJSRuntime.h"
+#include "mozilla/ThreadLocal.h"
 #include "mozilla/TimeStamp.h"
+
 #include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseDebugging.h"
 #include "mozilla/dom/PromiseDebuggingBinding.h"
 
 namespace mozilla {
 namespace dom {
 
+class FlushRejections: public nsCancelableRunnable
+{
+public:
+  static void Init() {
+    if (!sDispatched.init()) {
+      MOZ_CRASH("Could not initialize FlushRejections::sDispatched");
+    }
+    sDispatched.set(false);
+  }
+
+  static void DispatchNeeded() {
+    if (sDispatched.get()) {
+      // An instance of `FlushRejections` has already been dispatched
+      // and not run yet. No need to dispatch another one.
+      return;
+    }
+    sDispatched.set(true);
+    NS_DispatchToCurrentThread(new FlushRejections());
+  }
+
+  static void FlushSync() {
+    sDispatched.set(false);
+
+    // Call the callbacks if necessary.
+    // Note that these callbacks may in turn cause Promise to turn
+    // uncaught or consumed. Since `sDispatched` is `false`,
+    // `FlushRejections` will be called once again, on an ulterior
+    // tick.
+    PromiseDebugging::FlushUncaughtRejectionsInternal();
+  }
+
+  NS_IMETHOD Run() {
+    FlushSync();
+    return NS_OK;
+  }
+
+private:
+  // `true` if an instance of `FlushRejections` is currently dispatched
+  // and has not been executed yet.
+  static ThreadLocal<bool> sDispatched;
+};
+
+/* static */ ThreadLocal<bool>
+FlushRejections::sDispatched;
+
 /* static */ void
 PromiseDebugging::GetState(GlobalObject&, Promise& aPromise,
                            PromiseDebuggingStateHolder& aState)
 {
   switch (aPromise.mState) {
   case Promise::Pending:
     aState.mState = PromiseDebuggingState::Pending;
     break;
@@ -32,16 +81,47 @@ PromiseDebugging::GetState(GlobalObject&
   case Promise::Rejected:
     aState.mState = PromiseDebuggingState::Rejected;
     JS::ExposeValueToActiveJS(aPromise.mResult);
     aState.mReason = aPromise.mResult;
     break;
   }
 }
 
+/*static */ nsString
+PromiseDebugging::sIDPrefix;
+
+/* static */ void
+PromiseDebugging::Init()
+{
+  FlushRejections::Init();
+
+  // Generate a prefix for identifiers: "PromiseDebugging.$processid."
+  sIDPrefix = NS_LITERAL_STRING("PromiseDebugging.");
+  if (XRE_GetProcessType() == GeckoProcessType_Content) {
+    sIDPrefix.AppendInt(ContentChild::GetSingleton()->GetID());
+    sIDPrefix.Append('.');
+  } else {
+    sIDPrefix.AppendLiteral("0.");
+  }
+}
+
+/* static */ void
+PromiseDebugging::Shutdown()
+{
+  sIDPrefix.SetIsVoid(true);
+}
+
+/* static */ void
+PromiseDebugging::FlushUncaughtRejections()
+{
+  MOZ_ASSERT(!NS_IsMainThread());
+  FlushRejections::FlushSync();
+}
+
 /* static */ void
 PromiseDebugging::GetAllocationStack(GlobalObject&, Promise& aPromise,
                                      JS::MutableHandle<JSObject*> aStack)
 {
   aStack.set(aPromise.mAllocationStack);
 }
 
 /* static */ void
@@ -78,10 +158,124 @@ PromiseDebugging::GetTimeToSettle(Global
   if (aPromise.mState == Promise::Pending) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return 0;
   }
   return (aPromise.mSettlementTimestamp -
           aPromise.mCreationTimestamp).ToMilliseconds();
 }
 
+/* static */ void
+PromiseDebugging::AddUncaughtRejectionObserver(GlobalObject&,
+                                               UncaughtRejectionObserver& aObserver)
+{
+  CycleCollectedJSRuntime* storage = CycleCollectedJSRuntime::Get();
+  nsTArray<nsCOMPtr<nsISupports>>& observers = storage->mUncaughtRejectionObservers;
+  observers.AppendElement(&aObserver);
+}
+
+/* static */ bool
+PromiseDebugging::RemoveUncaughtRejectionObserver(GlobalObject&,
+                                                  UncaughtRejectionObserver& aObserver)
+{
+  CycleCollectedJSRuntime* storage = CycleCollectedJSRuntime::Get();
+  nsTArray<nsCOMPtr<nsISupports>>& observers = storage->mUncaughtRejectionObservers;
+  for (size_t i = 0; i < observers.Length(); ++i) {
+    UncaughtRejectionObserver* observer = static_cast<UncaughtRejectionObserver*>(observers[i].get());
+    if (*observer == aObserver) {
+      observers.RemoveElementAt(i);
+      return true;
+    }
+  }
+  return false;
+}
+
+/* static */ void
+PromiseDebugging::AddUncaughtRejection(Promise& aPromise)
+{
+  CycleCollectedJSRuntime::Get()->mUncaughtRejections.AppendElement(&aPromise);
+  FlushRejections::DispatchNeeded();
+}
+
+/* void */ void
+PromiseDebugging::AddConsumedRejection(Promise& aPromise)
+{
+  CycleCollectedJSRuntime::Get()->mConsumedRejections.AppendElement(&aPromise);
+  FlushRejections::DispatchNeeded();
+}
+
+/* static */ void
+PromiseDebugging::GetPromiseID(GlobalObject&,
+                               Promise& aPromise,
+                               nsString& aID)
+{
+  uint64_t promiseID = aPromise.GetID();
+  aID = sIDPrefix;
+  aID.AppendInt(promiseID);
+}
+
+/* static */ void
+PromiseDebugging::FlushUncaughtRejectionsInternal()
+{
+  CycleCollectedJSRuntime* storage = CycleCollectedJSRuntime::Get();
+
+  // The Promise that have been left uncaught (rejected and last in
+  // their chain) since the last call to this function.
+  nsTArray<nsCOMPtr<nsISupports>> uncaught;
+  storage->mUncaughtRejections.SwapElements(uncaught);
+
+  // The Promise that have been left uncaught at some point, but that
+  // have eventually had their `then` method called.
+  nsTArray<nsCOMPtr<nsISupports>> consumed;
+  storage->mConsumedRejections.SwapElements(consumed);
+
+  nsTArray<nsCOMPtr<nsISupports>>& observers = storage->mUncaughtRejectionObservers;
+
+  nsresult rv;
+  // Notify observers of uncaught Promise.
+
+  for (size_t i = 0; i < uncaught.Length(); ++i) {
+    nsCOMPtr<Promise> promise = do_QueryInterface(uncaught[i], &rv);
+    MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+    if (!promise->IsLastInChain()) {
+      // This promise is not the last in the chain anymore,
+      // so the error has been caught at some point.
+      continue;
+    }
+
+    // For the moment, the Promise is still at the end of the
+    // chain. Let's inform observers, so that they may decide whether
+    // to report it.
+    for (size_t j = 0; j < observers.Length(); ++j) {
+      ErrorResult err;
+      nsRefPtr<UncaughtRejectionObserver> obs =
+        static_cast<UncaughtRejectionObserver*>(observers[j].get());
+
+      obs->OnLeftUncaught(*promise, err); // Ignore errors
+    }
+
+    promise->SetNotifiedAsUncaught();
+  }
+
+  // Notify observers of consumed Promise.
+
+  for (size_t i = 0; i < consumed.Length(); ++i) {
+    nsCOMPtr<Promise> promise = do_QueryInterface(consumed[i], &rv);
+    MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+    if (!promise->WasNotifiedAsUncaught()) {
+      continue;
+    }
+
+    MOZ_ASSERT(!promise->IsLastInChain());
+    for (size_t j = 0; j < observers.Length(); ++j) {
+      ErrorResult err;
+      nsRefPtr<UncaughtRejectionObserver> obs =
+        static_cast<UncaughtRejectionObserver*>(observers[j].get());
+
+      obs->OnConsumed(*promise, err); // Ignore errors
+    }
+  }
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/promise/PromiseDebugging.h
+++ b/dom/promise/PromiseDebugging.h
@@ -11,36 +11,75 @@
 #include "nsTArray.h"
 #include "nsRefPtr.h"
 
 namespace mozilla {
 
 class ErrorResult;
 
 namespace dom {
+namespace workers {
+class WorkerPrivate;
+}
 
 class Promise;
 struct PromiseDebuggingStateHolder;
 class GlobalObject;
+class UncaughtRejectionObserver;
+class FlushRejections;
 
 class PromiseDebugging
 {
 public:
+  static void Init();
+  static void Shutdown();
+
   static void GetState(GlobalObject&, Promise& aPromise,
                        PromiseDebuggingStateHolder& aState);
 
   static void GetAllocationStack(GlobalObject&, Promise& aPromise,
                                  JS::MutableHandle<JSObject*> aStack);
   static void GetRejectionStack(GlobalObject&, Promise& aPromise,
                                 JS::MutableHandle<JSObject*> aStack);
   static void GetFullfillmentStack(GlobalObject&, Promise& aPromise,
                                    JS::MutableHandle<JSObject*> aStack);
   static void GetDependentPromises(GlobalObject&, Promise& aPromise,
                                    nsTArray<nsRefPtr<Promise>>& aPromises);
   static double GetPromiseLifetime(GlobalObject&, Promise& aPromise);
   static double GetTimeToSettle(GlobalObject&, Promise& aPromise,
                                 ErrorResult& aRv);
+
+  static void GetPromiseID(GlobalObject&, Promise&, nsString&);
+
+  // Mechanism for watching uncaught instances of Promise.
+  static void AddUncaughtRejectionObserver(GlobalObject&,
+                                           UncaughtRejectionObserver& aObserver);
+  static bool RemoveUncaughtRejectionObserver(GlobalObject&,
+                                              UncaughtRejectionObserver& aObserver);
+
+  // Mark a Promise as having been left uncaught at script completion.
+  static void AddUncaughtRejection(Promise&);
+  // Mark a Promise previously added with `AddUncaughtRejection` as
+  // eventually consumed.
+  static void AddConsumedRejection(Promise&);
+  // Propagate the informations from AddUncaughtRejection
+  // and AddConsumedRejection to observers.
+  static void FlushUncaughtRejections();
+
+protected:
+  static void FlushUncaughtRejectionsInternal();
+  friend class FlushRejections;
+  friend class WorkerPrivate;
+private:
+  // Identity of the process.
+  // This property is:
+  // - set during initialization of the layout module,
+  // prior to any Worker using it;
+  // - read by both the main thread and the Workers;
+  // - unset during shutdown of the layout module,
+  // after any Worker has been shutdown.
+  static nsString sIDPrefix;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_PromiseDebugging_h
--- a/dom/promise/moz.build
+++ b/dom/promise/moz.build
@@ -19,16 +19,20 @@ UNIFIED_SOURCES += [
     'PromiseCallback.cpp',
     'PromiseDebugging.cpp'
 ]
 
 FAIL_ON_WARNINGS = True
 
 LOCAL_INCLUDES += [
     '../base',
+    '../ipc',
     '../workers',
 ]
 
+include('/ipc/chromium/chromium-config.mozbuild')
+
 FINAL_LIBRARY = 'xul'
 
 MOCHITEST_MANIFESTS += ['tests/mochitest.ini']
 
 MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['tests/browser.ini']
new file mode 100644
--- /dev/null
+++ b/dom/promise/tests/browser.ini
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+
+[browser_monitorUncaught.js]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/promise/tests/browser_monitorUncaught.js
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Timer.jsm", this);
+
+add_task(function* test_globals() {
+  Assert.equal(Promise.defer || undefined, undefined, "We are testing DOM Promise.");
+  Assert.notEqual(PromiseDebugging, undefined, "PromiseDebugging is available.");
+});
+
+add_task(function* test_promiseID() {
+  let p1 = new Promise(resolve => {});
+  let p2 = new Promise(resolve => {});
+  let p3 = p2.then(null, null);
+  let promise = [p1, p2, p3];
+
+  let identifiers = promise.map(PromiseDebugging.getPromiseID);
+  info("Identifiers: " + JSON.stringify(identifiers));
+  let idSet = new Set(identifiers);
+  Assert.equal(idSet.size, identifiers.length,
+    "PromiseDebugging.getPromiseID returns a distinct id per promise");
+
+  let identifiers2 = promise.map(PromiseDebugging.getPromiseID);
+  Assert.equal(JSON.stringify(identifiers),
+               JSON.stringify(identifiers2),
+               "Successive calls to PromiseDebugging.getPromiseID return the same id for the same promise");
+});
+
+add_task(function* test_observe_uncaught() {
+  // The names of Promise instances
+  let names = new Map();
+
+  // The results for UncaughtPromiseObserver callbacks.
+  let CallbackResults = function(name) {
+    this.name = name;
+    this.expected = new Set();
+    this.observed = new Set();
+    this.blocker = new Promise(resolve => this.resolve = resolve);
+  };
+  CallbackResults.prototype = {
+    observe: function(promise) {
+      info(this.name + " observing Promise " + names.get(promise));
+      Assert.equal(PromiseDebugging.getState(promise).state, "rejected",
+                   this.name + " observed a rejected Promise");
+      if (!this.expected.has(promise)) {
+        Assert.ok(false,
+            this.name + " observed a Promise that it expected to observe, " +
+            names.get(promise) +
+            " (" + PromiseDebugging.getPromiseID(promise) +
+            ", " + PromiseDebugging.getAllocationStack(promise) + ")");
+
+      }
+      Assert.ok(this.expected.delete(promise),
+                this.name + " observed a Promise that it expected to observe, " +
+                names.get(promise)  + " (" + PromiseDebugging.getPromiseID(promise) + ")");
+      Assert.ok(!this.observed.has(promise),
+                this.name + " observed a Promise that it has not observed yet");
+      this.observed.add(promise);
+      if (this.expected.size == 0) {
+        this.resolve();
+      } else {
+        info(this.name + " is still waiting for " + this.expected.size + " observations:");
+        info(JSON.stringify([names.get(x) for (x of this.expected.values())]));
+      }
+    },
+  };
+
+  let onLeftUncaught = new CallbackResults("onLeftUncaught");
+  let onConsumed = new CallbackResults("onConsumed");
+
+  let observer = {
+    onLeftUncaught: function(promise, data) {
+      onLeftUncaught.observe(promise);
+    },
+    onConsumed: function(promise) {
+      onConsumed.observe(promise);
+    },
+  };
+
+  let resolveLater = function(delay = 20) {
+    return new Promise((resolve, reject) => setTimeout(resolve, delay));
+  };
+  let rejectLater = function(delay = 20) {
+    return new Promise((resolve, reject) => setTimeout(reject, delay));
+  };
+  let makeSamples = function*() {
+    yield {
+      promise: Promise.resolve(0),
+      name: "Promise.resolve",
+    };
+    yield {
+      promise: Promise.resolve(resolve => resolve(0)),
+      name: "Resolution callback",
+    };
+    yield {
+      promise: Promise.resolve(0).then(null, null),
+      name: "`then(null, null)`"
+    };
+    yield {
+      promise: Promise.reject(0).then(null, () => {}),
+      name: "Reject and catch immediately",
+    };
+    yield {
+      promise: resolveLater(),
+      name: "Resolve later",
+    };
+    yield {
+      promise: Promise.reject("Simple rejection"),
+      leftUncaught: true,
+      consumed: false,
+      name: "Promise.reject",
+    };
+
+    // Reject a promise now, consume it later.
+    let p = Promise.reject("Reject now, consume later");
+    setTimeout(() => p.then(null, () => {
+      info("Consumed promise");
+    }), 200);
+    yield {
+      promise: p,
+      leftUncaught: true,
+      consumed: true,
+      name: "Reject now, consume later",
+    };
+
+    yield {
+      promise: Promise.all([
+        Promise.resolve("Promise.all"),
+        rejectLater()
+      ]),
+      leftUncaught: true,
+      name: "Rejecting through Promise.all"
+    };
+    yield {
+      promise: Promise.race([
+        resolveLater(500),
+        Promise.reject(),
+      ]),
+      leftUncaught: true, // The rejection wins the race.
+      name: "Rejecting through Promise.race",
+    };
+    yield {
+      promise: Promise.race([
+        Promise.resolve(),
+        rejectLater(500)
+      ]),
+      leftUncaught: false, // The resolution wins the race.
+      name: "Resolving through Promise.race",
+    };
+
+    let boom = new Error("`throw` in the constructor");
+    yield {
+      promise: new Promise(() => { throw boom; }),
+      leftUncaught: true,
+      name: "Throwing in the constructor",
+    };
+
+    let rejection = Promise.reject("`reject` during resolution");
+    yield {
+      promise: rejection,
+      leftUncaught: false,
+      consumed: false, // `rejection` is consumed immediately (see below)
+      name: "Promise.reject, again",
+    };
+
+    yield {
+      promise: new Promise(resolve => resolve(rejection)),
+      leftUncaught: true,
+      consumed: false,
+      name: "Resolving with a rejected promise",
+    };
+
+    yield {
+      promise: Promise.resolve(0).then(() => rejection),
+      leftUncaught: true,
+      consumed: false,
+      name: "Returning a rejected promise from success handler",
+    };
+
+    yield {
+      promise: Promise.resolve(0).then(() => { throw new Error(); }),
+      leftUncaught: true,
+      consumed: false,
+      name: "Throwing during the call to the success callback",
+    };
+  };
+  let samples = [];
+  for (let s of makeSamples()) {
+    samples.push(s);
+    info("Promise '" + s.name + "' has id " + PromiseDebugging.getPromiseID(s.promise));
+  }
+
+  PromiseDebugging.addUncaughtRejectionObserver(observer);
+
+  for (let s of samples) {
+    names.set(s.promise, s.name);
+    if (s.leftUncaught || false) {
+      onLeftUncaught.expected.add(s.promise);
+    }
+    if (s.consumed || false) {
+      onConsumed.expected.add(s.promise);
+    }
+  }
+
+  info("Test setup, waiting for callbacks.");
+  yield onLeftUncaught.blocker;
+
+  info("All calls to onLeftUncaught are complete.");
+  if (onConsumed.expected.size != 0) {
+    info("onConsumed is still waiting for the following Promise:");
+    info(JSON.stringify([names.get(x) for (x of onConsumed.expected.values())]));
+    yield onConsumed.blocker;
+  }
+
+  info("All calls to onConsumed are complete.");
+  let removed = PromiseDebugging.removeUncaughtRejectionObserver(observer);
+  Assert.ok(removed, "removeUncaughtRejectionObserver succeeded");
+  removed = PromiseDebugging.removeUncaughtRejectionObserver(observer);
+  Assert.ok(!removed, "second call to removeUncaughtRejectionObserver didn't remove anything");
+});
+
+
+add_task(function* test_uninstall_observer() {
+  let Observer = function() {
+    this.blocker = new Promise(resolve => this.resolve = resolve);
+    this.active = true;
+  };
+  Observer.prototype = {
+    set active(x) {
+      this._active = x;
+      if (x) {
+        PromiseDebugging.addUncaughtRejectionObserver(this);
+      } else {
+        PromiseDebugging.removeUncaughtRejectionObserver(this);
+      }
+    },
+    onLeftUncaught: function() {
+      Assert.ok(this._active, "This observer is active.");
+      this.resolve();
+    },
+    onConsumed: function() {
+      Assert.ok(false, "We should not consume any Promise.");
+    },
+  };
+
+  info("Adding an observer.");
+  let deactivate = new Observer();
+  Promise.reject("I am an uncaught rejection.");
+  yield deactivate.blocker;
+  Assert.ok(true, "The observer has observed an uncaught Promise.");
+  deactivate.active = false;
+  info("Removing the observer, it should not observe any further uncaught Promise.");
+
+  info("Rejecting a Promise and waiting a little to give a chance to observers.");
+  let wait = new Observer();
+  Promise.reject("I am another uncaught rejection.");
+  yield wait.blocker;
+  yield new Promise(resolve => setTimeout(resolve, 100));
+  // Normally, `deactivate` should not be notified of the uncaught rejection.
+  wait.active = false;
+
+});
+
--- a/dom/webidl/PromiseDebugging.webidl
+++ b/dom/webidl/PromiseDebugging.webidl
@@ -9,16 +9,52 @@
 
 dictionary PromiseDebuggingStateHolder {
   PromiseDebuggingState state = "pending";
   any value;
   any reason;
 };
 enum PromiseDebuggingState { "pending", "fulfilled", "rejected" };
 
+/**
+ * An observer for Promise that _may_ be leaking uncaught rejections.
+ *
+ * It is generally a programming error to leave a Promise rejected and
+ * not consume its rejection. The information exposed by this
+ * interface is designed to allow clients to track down such Promise,
+ * i.e. Promise that are currently
+ * - in `rejected` state;
+ * - last of their chain.
+ *
+ * Note, however, that a promise in such a state at the end of a tick
+ * may eventually be consumed in some ulterior tick. Implementers of
+ * this interface are responsible for presenting the information
+ * in a meaningful manner.
+ */
+callback interface UncaughtRejectionObserver {
+  /**
+   * A Promise has been left in `rejected` state and is the
+   * last in its chain.
+   *
+   * @param p A currently uncaught Promise. If `p` is is eventually
+   * caught, i.e. if its `then` callback is called, `onConsumed` will
+   * be called.
+   */
+  void onLeftUncaught(Promise<any> p);
+
+  /**
+   * A Promise previously left uncaught is not the last in its
+   * chain anymore.
+   *
+   * @param p A Promise that was previously left in uncaught state is
+   * now caught, i.e. it is not the last in its chain anymore.
+   */
+  void onConsumed(Promise<any> p);
+};
+
 [ChromeOnly, Exposed=(Window,System)]
 interface PromiseDebugging {
   static PromiseDebuggingStateHolder getState(Promise<any> p);
 
   /**
    * Return the stack to the promise's allocation point.  This can
    * return null if the promise was not created from script.
    */
@@ -34,16 +70,22 @@ interface PromiseDebugging {
   /**
    * Return the stack to the promise's fulfillment point, if the
    * fulfillment happened from script.  This can return null if the
    * promise has not been fulfilled or was not fulfilled from script.
    */
   static object? getFullfillmentStack(Promise<any> p);
 
   /**
+   * Return an identifier for a promise. This identifier is guaranteed
+   * to be unique to this instance of Firefox.
+   */
+  static DOMString getPromiseID(Promise<any> p);
+
+  /**
    * Get the promises directly depending on a given promise.  These are:
    *
    * 1) Return values of then() calls on the promise
    * 2) Return values of Promise.all() if the given promise was passed in as one
    *    of the arguments.
    * 3) Return values of Promise.race() if the given promise was passed in as
    *    one of the arguments.
    *
@@ -63,9 +105,18 @@ interface PromiseDebugging {
 
   /*
    * Get the number of milliseconds elapsed between the promise being created
    * and being settled.  Throws NS_ERROR_UNEXPECTED if the promise has not
    * settled.
    */
   [Throws]
   static DOMHighResTimeStamp getTimeToSettle(Promise<any> p);
+
+  /**
+   * Watching uncaught rejections on the current thread.
+   *
+   * Adding an observer twice will cause it to be notified twice
+   * of events.
+   */
+  static void addUncaughtRejectionObserver(UncaughtRejectionObserver o);
+  static boolean removeUncaughtRejectionObserver(UncaughtRejectionObserver o);
 };
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -49,16 +49,17 @@
 #include "mozilla/dom/Exceptions.h"
 #include "mozilla/dom/FunctionBinding.h"
 #include "mozilla/dom/ImageData.h"
 #include "mozilla/dom/ImageDataBinding.h"
 #include "mozilla/dom/MessageEvent.h"
 #include "mozilla/dom/MessageEventBinding.h"
 #include "mozilla/dom/MessagePortList.h"
 #include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseDebugging.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/StructuredClone.h"
 #include "mozilla/dom/WebCryptoCommon.h"
 #include "mozilla/dom/WorkerBinding.h"
 #include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h"
 #include "mozilla/dom/WorkerGlobalScopeBinding.h"
 #include "mozilla/dom/indexedDB/IDBFactory.h"
 #include "mozilla/dom/ipc/BlobChild.h"
@@ -5154,16 +5155,20 @@ WorkerPrivate::DoRunLoop(JSContext* aCx)
         MOZ_ASSERT(currentStatus == Killing);
 #else
         currentStatus = Killing;
 #endif
       }
 
       // If we're supposed to die then we should exit the loop.
       if (currentStatus == Killing) {
+        // Flush uncaught rejections immediately, without
+        // waiting for a next tick.
+        PromiseDebugging::FlushUncaughtRejections();
+
         ShutdownGCTimers();
 
         DisableMemoryReporter();
 
         {
           MutexAutoLock lock(mMutex);
 
           mStatus = Dead;
--- a/layout/build/nsLayoutStatics.cpp
+++ b/layout/build/nsLayoutStatics.cpp
@@ -65,16 +65,17 @@
 #include "ActiveLayerTracker.h"
 #include "CounterStyleManager.h"
 #include "FrameLayerBuilder.h"
 #include "mozilla/dom/RequestSyncWifiService.h"
 #include "AnimationCommon.h"
 
 #include "AudioChannelService.h"
 #include "mozilla/dom/DataStoreService.h"
+#include "mozilla/dom/PromiseDebugging.h"
 
 #ifdef MOZ_XUL
 #include "nsXULPopupManager.h"
 #include "nsXULContentUtils.h"
 #include "nsXULPrototypeCache.h"
 #include "nsXULTooltipListener.h"
 
 #include "inDOMView.h"
@@ -312,16 +313,18 @@ nsLayoutStatics::Initialize()
 
 #ifdef DEBUG
   nsStyleContext::Initialize();
   mozilla::css::CommonAnimationManager::Initialize();
 #endif
 
   MediaDecoder::InitStatics();
 
+  PromiseDebugging::Init();
+
   return NS_OK;
 }
 
 void
 nsLayoutStatics::Shutdown()
 {
   // Don't need to shutdown nsWindowMemoryReporter, that will be done by the
   // memory reporter manager.
@@ -445,9 +448,11 @@ nsLayoutStatics::Shutdown()
 
   DisplayItemClip::Shutdown();
 
   nsDocument::XPCOMShutdown();
 
   CacheObserver::Shutdown();
 
   CameraPreferences::Shutdown();
+
+  PromiseDebugging::Shutdown();
 }
--- a/xpcom/base/CycleCollectedJSRuntime.cpp
+++ b/xpcom/base/CycleCollectedJSRuntime.cpp
@@ -1254,8 +1254,9 @@ CycleCollectedJSRuntime::OnOutOfMemory()
 
 void
 CycleCollectedJSRuntime::OnLargeAllocationFailure()
 {
   AnnotateAndSetOutOfMemory(&mLargeAllocationFailureState, OOMState::Reporting);
   CustomLargeAllocationFailureCallback();
   AnnotateAndSetOutOfMemory(&mLargeAllocationFailureState, OOMState::Reported);
 }
+
--- a/xpcom/base/CycleCollectedJSRuntime.h
+++ b/xpcom/base/CycleCollectedJSRuntime.h
@@ -287,16 +287,25 @@ public:
     MOZ_ASSERT(mJSRuntime);
     return mJSRuntime;
   }
 
   // Get the current thread's CycleCollectedJSRuntime.  Returns null if there
   // isn't one.
   static CycleCollectedJSRuntime* Get();
 
+  // Storage for watching rejected promises waiting for some client to
+  // consume their rejection.
+  // We store values as `nsISupports` to avoid adding compile-time dependencies
+  // from xpcom to dom/promise, but they can really only have a single concrete
+  // type.
+  nsTArray<nsCOMPtr<nsISupports /* Promise */>> mUncaughtRejections;
+  nsTArray<nsCOMPtr<nsISupports /* Promise */ >> mConsumedRejections;
+  nsTArray<nsCOMPtr<nsISupports /* UncaughtRejectionObserver */ >> mUncaughtRejectionObservers;
+
 private:
   JSGCThingParticipant mGCThingCycleCollectorGlobal;
 
   JSZoneParticipant mJSZoneCycleCollectorGlobal;
 
   JSRuntime* mJSRuntime;
 
   nsDataHashtable<nsPtrHashKey<void>, nsScriptObjectTracer*> mJSHolders;