diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js
index cb2bc456dbb8f1..b4e9906e8c3bc2 100644
--- a/lib/internal/modules/esm/hooks.js
+++ b/lib/internal/modules/esm/hooks.js
@@ -6,6 +6,7 @@ const {
   AtomicsLoad,
   AtomicsWait,
   AtomicsWaitAsync,
+  FunctionPrototypeBind,
   Int32Array,
   ObjectAssign,
   ObjectDefineProperty,
@@ -162,7 +163,7 @@ class Hooks {
    * to the worker.
    * @returns {any | Promise<any>} User data, ignored unless it's a promise, in which case it will be awaited.
    */
-  addCustomLoader(url, exports, data) {
+  async addCustomLoader(url, exports, data) {
     const {
       initialize,
       resolve,
@@ -177,7 +178,30 @@ class Hooks {
       const next = this.#chains.load[this.#chains.load.length - 1];
       ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
     }
-    return initialize?.(data);
+
+    const hooks = await initialize?.(data);
+
+    if (hooks?.resolve) {
+      if (resolve) {
+        throw new ERR_INTERNAL_ASSERTION(
+          `ESM custom loader '${url}' exposed a 'resolve' hook and returned an object with a 'resolve' hook.`,
+        );
+      }
+      const next = this.#chains.resolve[this.#chains.resolve.length - 1];
+      const boundResolve = FunctionPrototypeBind(hooks.resolve, hooks);
+      ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: boundResolve, url, next });
+    }
+
+    if (hooks?.load) {
+      if (load) {
+        throw new ERR_INTERNAL_ASSERTION(
+          `ESM custom loader '${url}' exposed a 'load' hook and returned an object with a 'load' hook.`,
+        );
+      }
+      const next = this.#chains.load[this.#chains.load.length - 1];
+      const boundLoad = FunctionPrototypeBind(hooks.load, hooks);
+      ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: boundLoad, url, next });
+    }
   }
 
   /**