diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
index 3374224cca0fc..cc925dad312b0 100644
--- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
@@ -815,6 +815,130 @@ describe('InspectedElement', () => {
     `);
   });
 
+  it('should support Thenables in React 19', async () => {
+    const Example = () => null;
+
+    class SubclassedPromise extends Promise {}
+
+    const plainThenable = {then() {}};
+    const subclassedPromise = new SubclassedPromise(() => {});
+    const unusedPromise = Promise.resolve();
+    const usedFulfilledPromise = Promise.resolve();
+    const usedFulfilledRichPromise = Promise.resolve({
+      some: {
+        deeply: {
+          nested: {
+            object: {
+              string: 'test',
+              fn: () => {},
+            },
+          },
+        },
+      },
+    });
+    const usedPendingPromise = new Promise(resolve => {});
+    const usedRejectedPromise = Promise.reject(
+      new Error('test-error-do-not-surface'),
+    );
+
+    function Use({value}) {
+      React.use(value);
+    }
+
+    await utils.actAsync(() =>
+      render(
+        <>
+          
+          
+            
+          
+          
+            
+          
+          
+            
+          
+          
+            
+              
+            
+          
+        >,
+      ),
+    );
+
+    const inspectedElement = await inspectElementAtIndex(0);
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+      {
+        "plainThenable": Dehydrated {
+          "preview_short": Thenable,
+          "preview_long": Thenable,
+        },
+        "subclassedPromise": Dehydrated {
+          "preview_short": SubclassedPromise,
+          "preview_long": SubclassedPromise,
+        },
+        "unusedPromise": Dehydrated {
+          "preview_short": Promise,
+          "preview_long": Promise,
+        },
+        "usedFulfilledPromise": {
+          "value": undefined,
+        },
+        "usedFulfilledRichPromise": {
+          "value": Dehydrated {
+            "preview_short": {…},
+            "preview_long": {some: {…}},
+          },
+        },
+        "usedPendingPromise": Dehydrated {
+          "preview_short": pending Promise,
+          "preview_long": pending Promise,
+        },
+        "usedRejectedPromise": {
+          "reason": Dehydrated {
+            "preview_short": Error,
+            "preview_long": Error,
+          },
+        },
+      }
+    `);
+  });
+
+  it('should support Promises in React 18', async () => {
+    const Example = () => null;
+
+    const unusedPromise = Promise.resolve();
+
+    await utils.actAsync(() =>
+      render(
+        <>
+          
+        >,
+      ),
+    );
+
+    const inspectedElement = await inspectElementAtIndex(0);
+
+    expect(inspectedElement.props).toMatchInlineSnapshot(`
+    {
+      "unusedPromise": Dehydrated {
+        "preview_short": Promise,
+        "preview_long": Promise,
+      },
+    }
+  `);
+  });
+
   it('should not consume iterables while inspecting', async () => {
     const Example = () => null;
 
diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js
index c21efe40a88fa..3df47c68bd7d6 100644
--- a/packages/react-devtools-shared/src/hydration.js
+++ b/packages/react-devtools-shared/src/hydration.js
@@ -43,7 +43,7 @@ export type Dehydrated = {
   type: string,
 };
 
-// Typed arrays and other complex iteratable objects (e.g. Map, Set, ImmutableJS) need special handling.
+// Typed arrays, other complex iteratable objects (e.g. Map, Set, ImmutableJS) or Promises need special handling.
 // These objects can't be serialized without losing type information,
 // so a "Unserializable" type wrapper is used (with meta-data keys) to send nested values-
 // while preserving the original type and name.
@@ -303,6 +303,76 @@ export function dehydrate(
         type,
       };
 
+    case 'thenable':
+      isPathAllowedCheck = isPathAllowed(path);
+
+      if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
+        return {
+          inspectable:
+            data.status === 'fulfilled' || data.status === 'rejected',
+          preview_short: formatDataForPreview(data, false),
+          preview_long: formatDataForPreview(data, true),
+          name: data.toString(),
+          type,
+        };
+      }
+
+      switch (data.status) {
+        case 'fulfilled': {
+          const unserializableValue: Unserializable = {
+            unserializable: true,
+            type: type,
+            preview_short: formatDataForPreview(data, false),
+            preview_long: formatDataForPreview(data, true),
+            name: 'fulfilled Thenable',
+          };
+
+          unserializableValue.value = dehydrate(
+            data.value,
+            cleaned,
+            unserializable,
+            path.concat(['value']),
+            isPathAllowed,
+            isPathAllowedCheck ? 1 : level + 1,
+          );
+
+          unserializable.push(path);
+
+          return unserializableValue;
+        }
+        case 'rejected': {
+          const unserializableValue: Unserializable = {
+            unserializable: true,
+            type: type,
+            preview_short: formatDataForPreview(data, false),
+            preview_long: formatDataForPreview(data, true),
+            name: 'rejected Thenable',
+          };
+
+          unserializableValue.reason = dehydrate(
+            data.reason,
+            cleaned,
+            unserializable,
+            path.concat(['reason']),
+            isPathAllowed,
+            isPathAllowedCheck ? 1 : level + 1,
+          );
+
+          unserializable.push(path);
+
+          return unserializableValue;
+        }
+        default:
+          cleaned.push(path);
+          return {
+            inspectable: false,
+            preview_short: formatDataForPreview(data, false),
+            preview_long: formatDataForPreview(data, true),
+            name: data.toString(),
+            type,
+          };
+      }
+
     case 'object':
       isPathAllowedCheck = isPathAllowed(path);
 
diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js
index b0a8f5c53e0cd..ebb6265b1d668 100644
--- a/packages/react-devtools-shared/src/utils.js
+++ b/packages/react-devtools-shared/src/utils.js
@@ -563,6 +563,7 @@ export type DataType =
   | 'nan'
   | 'null'
   | 'number'
+  | 'thenable'
   | 'object'
   | 'react_element'
   | 'regexp'
@@ -631,6 +632,8 @@ export function getDataType(data: Object): DataType {
         }
       } else if (data.constructor && data.constructor.name === 'RegExp') {
         return 'regexp';
+      } else if (typeof data.then === 'function') {
+        return 'thenable';
       } else {
         // $FlowFixMe[method-unbinding]
         const toStringValue = Object.prototype.toString.call(data);
@@ -934,6 +937,42 @@ export function formatDataForPreview(
       } catch (error) {
         return 'unserializable';
       }
+    case 'thenable':
+      let displayName: string;
+      if (isPlainObject(data)) {
+        displayName = 'Thenable';
+      } else {
+        let resolvedConstructorName = data.constructor.name;
+        if (typeof resolvedConstructorName !== 'string') {
+          resolvedConstructorName =
+            Object.getPrototypeOf(data).constructor.name;
+        }
+        if (typeof resolvedConstructorName === 'string') {
+          displayName = resolvedConstructorName;
+        } else {
+          displayName = 'Thenable';
+        }
+      }
+      switch (data.status) {
+        case 'pending':
+          return `pending ${displayName}`;
+        case 'fulfilled':
+          if (showFormattedValue) {
+            const formatted = formatDataForPreview(data.value, false);
+            return `fulfilled ${displayName} {${truncateForDisplay(formatted)}}`;
+          } else {
+            return `fulfilled ${displayName} {…}`;
+          }
+        case 'rejected':
+          if (showFormattedValue) {
+            const formatted = formatDataForPreview(data.reason, false);
+            return `rejected ${displayName} {${truncateForDisplay(formatted)}}`;
+          } else {
+            return `rejected ${displayName} {…}`;
+          }
+        default:
+          return displayName;
+      }
     case 'object':
       if (showFormattedValue) {
         const keys = Array.from(getAllEnumerableKeys(data)).sort(alphaSortKeys);
@@ -963,7 +1002,7 @@ export function formatDataForPreview(
     case 'nan':
     case 'null':
     case 'undefined':
-      return data;
+      return String(data);
     default:
       try {
         return truncateForDisplay(String(data));
diff --git a/packages/react-devtools-shell/README.md b/packages/react-devtools-shell/README.md
index da572de45364e..0a78ebc86b1d8 100644
--- a/packages/react-devtools-shell/README.md
+++ b/packages/react-devtools-shell/README.md
@@ -2,7 +2,7 @@ Harness for testing local changes to the `react-devtools-inline` and `react-devt
 
 ## Development
 
-This target should be run in parallel with the `react-devtools-inline` package. The first step then is to run that target following the instructions in the [`react-devtools-inline` README's local development section](https://github.com/facebook/react/tree/main/packages/react-devtools-inline#local-development).
+This target should be run in parallel with the `react-devtools-inline` package. The first step then is to run that target following the instructions in the [`react-devtools-inline` README's local development section](../react-devtools-inline/README.md#local-development).
 
 The test harness can then be run as follows:
 ```sh
diff --git a/packages/react-devtools-shell/src/app/Hydration/index.js b/packages/react-devtools-shell/src/app/Hydration/index.js
index 29c40c4aee2d2..227f8558ef42b 100644
--- a/packages/react-devtools-shell/src/app/Hydration/index.js
+++ b/packages/react-devtools-shell/src/app/Hydration/index.js
@@ -49,6 +49,10 @@ const objectOfObjects = {
     j: 9,
   },
   qux: {},
+  quux: {
+    k: undefined,
+    l: null,
+  },
 };
 
 function useOuterFoo() {
@@ -106,6 +110,26 @@ function useInnerBaz() {
   return count;
 }
 
+const unusedPromise = Promise.resolve();
+const usedFulfilledPromise = Promise.resolve();
+const usedFulfilledRichPromise = Promise.resolve({
+  some: {
+    deeply: {
+      nested: {
+        object: {
+          string: 'test',
+          fn: () => {},
+        },
+      },
+    },
+  },
+});
+const usedPendingPromise = new Promise(resolve => {});
+const usedRejectedPromise = Promise.reject(
+  // eslint-disable-next-line react-internal/prod-error-codes
+  new Error('test-error-do-not-surface'),
+);
+
 export default function Hydration(): React.Node {
   return (
     
@@ -120,17 +144,55 @@ export default function Hydration(): React.Node {
         date={new Date()}
         array={arrayOfArrays}
         object={objectOfObjects}
+        unusedPromise={unusedPromise}
+        usedFulfilledPromise={usedFulfilledPromise}
+        usedFulfilledRichPromise={usedFulfilledRichPromise}
+        usedPendingPromise={usedPendingPromise}
+        usedRejectedPromise={usedRejectedPromise}
       />
       
     
   );
 }
 
+function Use({value}: {value: Promise}): React.Node {
+  React.use(value);
+  return null;
+}
+
+class IgnoreErrors extends React.Component {
+  state: {hasError: boolean} = {hasError: false};
+  static getDerivedStateFromError(): {hasError: boolean} {
+    return {hasError: true};
+  }
+
+  render(): React.Node {
+    if (this.state.hasError) {
+      return null;
+    }
+    return this.props.children;
+  }
+}
+
 function DehydratableProps({array, object}: any) {
   return (
     
       - array: {JSON.stringify(array, null, 2)}
- object: {JSON.stringify(object, null, 2)}+      
+        
+      
+      
+        
+      
+      
+        
+      
+      
+        
+          
+        
+
);
 }
diff --git a/packages/react-devtools-shell/webpack-server.js b/packages/react-devtools-shell/webpack-server.js
index cd35ff3ed9a89..a665601b66010 100644
--- a/packages/react-devtools-shell/webpack-server.js
+++ b/packages/react-devtools-shell/webpack-server.js
@@ -176,6 +176,14 @@ const appServer = new WebpackDevServer(
       logging: 'warn',
       overlay: {
         warnings: false,
+        runtimeErrors: error => {
+          const shouldIgnoreError =
+            error !== null &&
+            typeof error === 'object' &&
+            error.message === 'test-error-do-not-surface';
+
+          return !shouldIgnoreError;
+        },
       },
     },
     static: {