diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md
index d6407dea2837b3..dd1eef22a96bd5 100644
--- a/doc/api/async_hooks.md
+++ b/doc/api/async_hooks.md
@@ -203,13 +203,16 @@ UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
 RANDOMBYTESREQUEST, TLSWRAP, Timeout, Immediate, TickObject
 ```
 
+There is also the `PROMISE` resource type, which is used to track `Promise`
+instances and asynchronous work scheduled by them.
+
 Users are be able to define their own `type` when using the public embedder API.
 
 *Note:* It is possible to have type name collisions. Embedders are encouraged
 to use a unique prefixes, such as the npm package name, to prevent collisions
 when listening to the hooks.
 
-###### `triggerid`
+###### `triggerId`
 
 `triggerId` is the `asyncId` of the resource that caused (or "triggered") the
 new resource to initialize and that caused `init` to call. This is different
@@ -258,7 +261,13 @@ considered public, but using the Embedder API users can provide and document
 their own resource objects. Such as resource object could for example contain
 the SQL query being executed.
 
-*Note:* In some cases the resource object is reused for performance reasons,
+In the case of Promises, the `resource` object will have `promise` property
+that refers to the Promise that is being initialized, and a `parentId` property
+that equals the `asyncId` of a parent Promise, if there is one, and
+`undefined` otherwise. For example, in the case of `b = a.then(handler)`,
+`a` is considered a parent Promise of `b`.
+
+*Note*: In some cases the resource object is reused for performance reasons,
 it is thus not safe to use it as a key in a `WeakMap` or add properties to it.
 
 ###### asynchronous context example
diff --git a/src/async-wrap.cc b/src/async-wrap.cc
index 813d7c21bb0d87..2dcfdc799720ed 100644
--- a/src/async-wrap.cc
+++ b/src/async-wrap.cc
@@ -36,6 +36,7 @@ using v8::Context;
 using v8::Float64Array;
 using v8::Function;
 using v8::FunctionCallbackInfo;
+using v8::FunctionTemplate;
 using v8::HandleScope;
 using v8::HeapProfiler;
 using v8::Integer;
@@ -44,8 +45,10 @@ using v8::Local;
 using v8::MaybeLocal;
 using v8::Number;
 using v8::Object;
+using v8::ObjectTemplate;
 using v8::Promise;
 using v8::PromiseHookType;
+using v8::PropertyCallbackInfo;
 using v8::RetainedObjectInfo;
 using v8::String;
 using v8::Symbol;
@@ -282,37 +285,86 @@ bool AsyncWrap::EmitAfter(Environment* env, double async_id) {
 class PromiseWrap : public AsyncWrap {
  public:
   PromiseWrap(Environment* env, Local<Object> object, bool silent)
-    : AsyncWrap(env, object, PROVIDER_PROMISE, silent) {}
+    : AsyncWrap(env, object, PROVIDER_PROMISE, silent) {
+    MakeWeak(this);
+  }
   size_t self_size() const override { return sizeof(*this); }
+
+  static constexpr int kPromiseField = 1;
+  static constexpr int kParentIdField = 2;
+  static constexpr int kInternalFieldCount = 3;
+
+  static PromiseWrap* New(Environment* env,
+                          Local<Promise> promise,
+                          PromiseWrap* parent_wrap,
+                          bool silent);
+  static void GetPromise(Local<String> property,
+                         const PropertyCallbackInfo<Value>& info);
+  static void GetParentId(Local<String> property,
+                          const PropertyCallbackInfo<Value>& info);
 };
 
+PromiseWrap* PromiseWrap::New(Environment* env,
+                              Local<Promise> promise,
+                              PromiseWrap* parent_wrap,
+                              bool silent) {
+  Local<Object> object = env->promise_wrap_template()
+                            ->NewInstance(env->context()).ToLocalChecked();
+  object->SetInternalField(PromiseWrap::kPromiseField, promise);
+  if (parent_wrap != nullptr) {
+    object->SetInternalField(PromiseWrap::kParentIdField,
+                             Number::New(env->isolate(),
+                                         parent_wrap->get_id()));
+  }
+  CHECK_EQ(promise->GetAlignedPointerFromInternalField(0), nullptr);
+  promise->SetInternalField(0, object);
+  return new PromiseWrap(env, object, silent);
+}
+
+void PromiseWrap::GetPromise(Local<String> property,
+                             const PropertyCallbackInfo<Value>& info) {
+  info.GetReturnValue().Set(info.Holder()->GetInternalField(kPromiseField));
+}
+
+void PromiseWrap::GetParentId(Local<String> property,
+                              const PropertyCallbackInfo<Value>& info) {
+  info.GetReturnValue().Set(info.Holder()->GetInternalField(kParentIdField));
+}
 
 static void PromiseHook(PromiseHookType type, Local<Promise> promise,
                         Local<Value> parent, void* arg) {
   Local<Context> context = promise->CreationContext();
   Environment* env = Environment::GetCurrent(context);
-  PromiseWrap* wrap = Unwrap<PromiseWrap>(promise);
+  Local<Value> resource_object_value = promise->GetInternalField(0);
+  PromiseWrap* wrap = nullptr;
+  if (resource_object_value->IsObject()) {
+    Local<Object> resource_object = resource_object_value.As<Object>();
+    wrap = Unwrap<PromiseWrap>(resource_object);
+  }
   if (type == PromiseHookType::kInit || wrap == nullptr) {
     bool silent = type != PromiseHookType::kInit;
+    PromiseWrap* parent_wrap = nullptr;
+
     // set parent promise's async Id as this promise's triggerId
     if (parent->IsPromise()) {
       // parent promise exists, current promise
       // is a chained promise, so we set parent promise's id as
       // current promise's triggerId
       Local<Promise> parent_promise = parent.As<Promise>();
-      auto parent_wrap = Unwrap<PromiseWrap>(parent_promise);
+      Local<Value> parent_resource = parent_promise->GetInternalField(0);
+      if (parent_resource->IsObject()) {
+        parent_wrap = Unwrap<PromiseWrap>(parent_resource.As<Object>());
+      }
 
       if (parent_wrap == nullptr) {
-        // create a new PromiseWrap for parent promise with silent parameter
-        parent_wrap = new PromiseWrap(env, parent_promise, true);
-        parent_wrap->MakeWeak(parent_wrap);
+        parent_wrap = PromiseWrap::New(env, parent_promise, nullptr, true);
       }
       // get id from parentWrap
       double trigger_id = parent_wrap->get_id();
       env->set_init_trigger_id(trigger_id);
     }
-    wrap = new PromiseWrap(env, promise, silent);
-    wrap->MakeWeak(wrap);
+
+    wrap = PromiseWrap::New(env, promise, parent_wrap, silent);
   } else if (type == PromiseHookType::kResolve) {
     // TODO(matthewloring): need to expose this through the async hooks api.
   }
@@ -351,6 +403,22 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
   SET_HOOK_FN(destroy);
   env->AddPromiseHook(PromiseHook, nullptr);
 #undef SET_HOOK_FN
+
+  {
+    Local<FunctionTemplate> ctor =
+        FunctionTemplate::New(env->isolate());
+    ctor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PromiseWrap"));
+    Local<ObjectTemplate> promise_wrap_template = ctor->InstanceTemplate();
+    promise_wrap_template->SetInternalFieldCount(
+        PromiseWrap::kInternalFieldCount);
+    promise_wrap_template->SetAccessor(
+        FIXED_ONE_BYTE_STRING(env->isolate(), "promise"),
+        PromiseWrap::GetPromise);
+    promise_wrap_template->SetAccessor(
+        FIXED_ONE_BYTE_STRING(env->isolate(), "parentId"),
+        PromiseWrap::GetParentId);
+    env->set_promise_wrap_template(promise_wrap_template);
+  }
 }
 
 
diff --git a/src/env.h b/src/env.h
index 7179a6081cc417..2eacf68ef5e93b 100644
--- a/src/env.h
+++ b/src/env.h
@@ -268,6 +268,7 @@ namespace node {
   V(pipe_constructor_template, v8::FunctionTemplate)                          \
   V(process_object, v8::Object)                                               \
   V(promise_reject_function, v8::Function)                                    \
+  V(promise_wrap_template, v8::ObjectTemplate)                                \
   V(push_values_to_array_function, v8::Function)                              \
   V(randombytes_constructor_template, v8::ObjectTemplate)                     \
   V(script_context_constructor_template, v8::FunctionTemplate)                \
diff --git a/test/parallel/test-async-hooks-promise.js b/test/parallel/test-async-hooks-promise.js
new file mode 100644
index 00000000000000..ea5e59d319db53
--- /dev/null
+++ b/test/parallel/test-async-hooks-promise.js
@@ -0,0 +1,23 @@
+'use strict';
+const common = require('../common');
+const assert = require('assert');
+const async_hooks = require('async_hooks');
+
+const initCalls = [];
+
+async_hooks.createHook({
+  init: common.mustCall((id, type, triggerId, resource) => {
+    assert.strictEqual(type, 'PROMISE');
+    initCalls.push({id, triggerId, resource});
+  }, 2)
+}).enable();
+
+const a = Promise.resolve(42);
+const b = a.then(common.mustCall());
+
+assert.strictEqual(initCalls[0].triggerId, 1);
+assert.strictEqual(initCalls[0].resource.parentId, undefined);
+assert.strictEqual(initCalls[0].resource.promise, a);
+assert.strictEqual(initCalls[1].triggerId, initCalls[0].id);
+assert.strictEqual(initCalls[1].resource.parentId, initCalls[0].id);
+assert.strictEqual(initCalls[1].resource.promise, b);