diff --git a/cli/tests/integration/node_unit_tests.rs b/cli/tests/integration/node_unit_tests.rs
index 273066b093..351bf1eecd 100644
--- a/cli/tests/integration/node_unit_tests.rs
+++ b/cli/tests/integration/node_unit_tests.rs
@@ -193,3 +193,12 @@ itest!(unhandled_rejection_web_process {
   envs: env_vars_for_npm_tests(),
   http_server: true,
 });
+
+// Ensure that Web `onrejectionhandled` is fired before
+// Node's `process.on('rejectionHandled')`.
+itest!(rejection_handled_web_process {
+  args: "run -A node/rejection_handled_web_process.ts",
+  output: "node/rejection_handled_web_process.ts.out",
+  envs: env_vars_for_npm_tests(),
+  http_server: true,
+});
diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs
index 2a349a5f26..999dc11778 100644
--- a/cli/tests/integration/run_tests.rs
+++ b/cli/tests/integration/run_tests.rs
@@ -3672,6 +3672,11 @@ itest!(unhandled_rejection_dynamic_import2 {
   output: "run/unhandled_rejection_dynamic_import2/main.ts.out",
 });
 
+itest!(rejection_handled {
+  args: "run --check run/rejection_handled.ts",
+  output: "run/rejection_handled.out",
+});
+
 itest!(nested_error {
   args: "run run/nested_error/main.ts",
   output: "run/nested_error/main.ts.out",
diff --git a/cli/tests/testdata/node/rejection_handled_web_process.ts b/cli/tests/testdata/node/rejection_handled_web_process.ts
new file mode 100644
index 0000000000..00d943feb9
--- /dev/null
+++ b/cli/tests/testdata/node/rejection_handled_web_process.ts
@@ -0,0 +1,26 @@
+import chalk from "npm:chalk";
+import process from "node:process";
+
+console.log(chalk.red("Hello world!"));
+
+globalThis.addEventListener("unhandledrejection", (e) => {
+  console.log('globalThis.addEventListener("unhandledrejection");');
+  e.preventDefault();
+});
+
+globalThis.addEventListener("rejectionhandled", (_) => {
+  console.log("Web rejectionhandled");
+});
+
+process.on("rejectionHandled", (_) => {
+  console.log("Node rejectionHandled");
+});
+
+const a = Promise.reject(1);
+setTimeout(() => {
+  a.catch(() => console.log("Added catch handler to the promise"));
+}, 10);
+
+setTimeout(() => {
+  console.log("Success");
+}, 50);
diff --git a/cli/tests/testdata/node/rejection_handled_web_process.ts.out b/cli/tests/testdata/node/rejection_handled_web_process.ts.out
new file mode 100644
index 0000000000..3a4e2ac234
--- /dev/null
+++ b/cli/tests/testdata/node/rejection_handled_web_process.ts.out
@@ -0,0 +1,7 @@
+[WILDCARD]
+Hello world!
+globalThis.addEventListener("unhandledrejection");
+Added catch handler to the promise
+Web rejectionhandled
+Node rejectionHandled
+Success
diff --git a/cli/tests/testdata/run/rejection_handled.out b/cli/tests/testdata/run/rejection_handled.out
new file mode 100644
index 0000000000..5c06fcd2b3
--- /dev/null
+++ b/cli/tests/testdata/run/rejection_handled.out
@@ -0,0 +1,5 @@
+[WILDCARD]
+unhandledrejection 1 Promise { <rejected> 1 }
+Added catch handler to the promise
+rejectionhandled 1 Promise { <rejected> 1 }
+Success
diff --git a/cli/tests/testdata/run/rejection_handled.ts b/cli/tests/testdata/run/rejection_handled.ts
new file mode 100644
index 0000000000..f058ff9665
--- /dev/null
+++ b/cli/tests/testdata/run/rejection_handled.ts
@@ -0,0 +1,17 @@
+window.addEventListener("unhandledrejection", (event) => {
+  console.log("unhandledrejection", event.reason, event.promise);
+  event.preventDefault();
+});
+
+window.addEventListener("rejectionhandled", (event) => {
+  console.log("rejectionhandled", event.reason, event.promise);
+});
+
+const a = Promise.reject(1);
+setTimeout(async () => {
+  a.catch(() => console.log("Added catch handler to the promise"));
+}, 10);
+
+setTimeout(() => {
+  console.log("Success");
+}, 50);
diff --git a/cli/tsc/dts/lib.deno.window.d.ts b/cli/tsc/dts/lib.deno.window.d.ts
index c518c53560..eaab7c3c23 100644
--- a/cli/tsc/dts/lib.deno.window.d.ts
+++ b/cli/tsc/dts/lib.deno.window.d.ts
@@ -12,6 +12,7 @@
 declare interface WindowEventMap {
   "error": ErrorEvent;
   "unhandledrejection": PromiseRejectionEvent;
+  "rejectionhandled": PromiseRejectionEvent;
 }
 
 /** @category Web APIs */
@@ -25,6 +26,9 @@ declare interface Window extends EventTarget {
   onunhandledrejection:
     | ((this: Window, ev: PromiseRejectionEvent) => any)
     | null;
+  onrejectionhandled:
+    | ((this: Window, ev: PromiseRejectionEvent) => any)
+    | null;
   close: () => void;
   readonly closed: boolean;
   alert: (message?: string) => void;
diff --git a/ext/node/polyfills/process.ts b/ext/node/polyfills/process.ts
index 3d5009b90b..1edcccc009 100644
--- a/ext/node/polyfills/process.ts
+++ b/ext/node/polyfills/process.ts
@@ -75,7 +75,6 @@ import { buildAllowedFlags } from "ext:deno_node/internal/process/per_thread.mjs
 
 const notImplementedEvents = [
   "multipleResolves",
-  "rejectionHandled",
   "worker",
 ];
 
@@ -746,6 +745,7 @@ export const removeListener = process.removeListener;
 export const removeAllListeners = process.removeAllListeners;
 
 let unhandledRejectionListenerCount = 0;
+let rejectionHandledListenerCount = 0;
 let uncaughtExceptionListenerCount = 0;
 let beforeExitListenerCount = 0;
 let exitListenerCount = 0;
@@ -755,6 +755,9 @@ process.on("newListener", (event: string) => {
     case "unhandledRejection":
       unhandledRejectionListenerCount++;
       break;
+    case "rejectionHandled":
+      rejectionHandledListenerCount++;
+      break;
     case "uncaughtException":
       uncaughtExceptionListenerCount++;
       break;
@@ -775,6 +778,9 @@ process.on("removeListener", (event: string) => {
     case "unhandledRejection":
       unhandledRejectionListenerCount--;
       break;
+    case "rejectionHandled":
+      rejectionHandledListenerCount--;
+      break;
     case "uncaughtException":
       uncaughtExceptionListenerCount--;
       break;
@@ -837,6 +843,16 @@ function synchronizeListeners() {
     internals.nodeProcessUnhandledRejectionCallback = undefined;
   }
 
+  // Install special "handledrejection" handler, that will be called
+  // last.
+  if (rejectionHandledListenerCount > 0) {
+    internals.nodeProcessRejectionHandledCallback = (event) => {
+      process.emit("rejectionHandled", event.reason, event.promise);
+    };
+  } else {
+    internals.nodeProcessRejectionHandledCallback = undefined;
+  }
+
   if (uncaughtExceptionListenerCount > 0) {
     globalThis.addEventListener("error", processOnError);
   } else {
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index 4644d2d08c..6c5ca3b590 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -344,6 +344,8 @@ function runtimeStart(
 }
 
 core.setUnhandledPromiseRejectionHandler(processUnhandledPromiseRejection);
+core.setHandledPromiseRejectionHandler(processRejectionHandled);
+
 // Notification that the core received an unhandled promise rejection that is about to
 // terminate the runtime. If we can handle it, attempt to do so.
 function processUnhandledPromiseRejection(promise, reason) {
@@ -377,6 +379,20 @@ function processUnhandledPromiseRejection(promise, reason) {
   return false;
 }
 
+function processRejectionHandled(promise, reason) {
+  const rejectionHandledEvent = new event.PromiseRejectionEvent(
+    "rejectionhandled",
+    { promise, reason },
+  );
+
+  // Note that the handler may throw, causing a recursive "error" event
+  globalThis_.dispatchEvent(rejectionHandledEvent);
+
+  if (typeof internals.nodeProcessRejectionHandledCallback !== "undefined") {
+    internals.nodeProcessRejectionHandledCallback(rejectionHandledEvent);
+  }
+}
+
 let hasBootstrapped = false;
 // Delete the `console` object that V8 automaticaly adds onto the global wrapper
 // object on context creation. We don't want this console object to shadow the