Repository: facebook/react
Pull request: [Fizz] Move digest from errorInfo to Error instance

As discussed in https://github.com/facebook/react/pull/25302#issuecomment-1255213588

There are reasons thrown or rejected values on the server may not always hit error pathways on the client. For uniformity we are moving towards having error digests which are generated on the server being exposed on the client direclty on the instance which represents the server error/reject.

This PR updates the Fizz Suspense boundary error reification on the client to stash the digest directly on the constructed Error instance. it also adds a deprecation warning if you read the digest off the `errorInfo` object in Dev.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
patch: @@ -92,7 +92,7 @@ describe('ReactDOMFizzServer', () => {
   function expectErrors(errorsArr, toBeDevArr, toBeProdArr) {
     const mappedErrows = errorsArr.map(({error, errorInfo}) => {
       const stack = errorInfo && errorInfo.componentStack;
-      const digest = errorInfo && errorInfo.digest;
+      const digest = error.digest;
       if (stack) {
         return [error.message, digest, normalizeCodeLocInfo(stack)];
       } else if (digest) {
@@ -3230,6 +3230,47 @@ describe('ReactDOMFizzServer', () => {
     );
   });
 
+  it('warns in dev if you access digest from errorInfo in onRecoverableError', async () => {
+    await act(async () => {
+      const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+        <div>
+          <Suspense fallback={'loading...'}>
+            <AsyncText text={'hello'} />
+          </Suspense>
+        </div>,
+        {
+          onError(error) {
+            return 'a digest';
+          },
+        },
+      );
+      rejectText('hello');
+      pipe(writable);
+    });
+    expect(getVisibleChildren(container)).toEqual(<div>loading...</div>);
+
+    ReactDOMClient.hydrateRoot(
+      container,
+      <div>
+        <Suspense fallback={'loading...'}>hello</Suspense>
+      </div>,
+      {
+        onRecoverableError(error, errorInfo) {
+          expect(() => {
+            expect(error.digest).toBe('a digest');
+            expect(errorInfo.digest).toBe('a digest');
+          }).toErrorDev(
+            'Warning: You are accessing "digest" from the errorInfo object passed to onRecoverableError.' +
+              ' This property is deprecated and will be removed in a future version of React.' +
+              ' To access the digest of an Error look for this property on the Error instance itself.',
+            {withoutStack: true},
+          );
+        },
+      },
+    );
+    expect(Scheduler).toFlushWithoutYielding();
+  });
+
   describe('error escaping', () => {
     it('escapes error hash, message, and component stack values in directly flushed errors (html escaping)', async () => {
       window.__outlet = {};

filename: packages/react-reconciler/src/ReactFiberBeginWork.new.js
patch: @@ -2746,6 +2746,7 @@ function updateDehydratedSuspenseComponent(
             'client rendering.',
         );
       }
+      (error: any).digest = digest;
       const capturedValue = createCapturedValue(error, digest, stack);
       return retrySuspenseComponentWithoutHydrating(
         current,

filename: packages/react-reconciler/src/ReactFiberBeginWork.old.js
patch: @@ -2746,6 +2746,7 @@ function updateDehydratedSuspenseComponent(
             'client rendering.',
         );
       }
+      (error: any).digest = digest;
       const capturedValue = createCapturedValue(error, digest, stack);
       return retrySuspenseComponentWithoutHydrating(
         current,

filename: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
patch: @@ -2596,9 +2596,11 @@ function commitRootImpl(
     const onRecoverableError = root.onRecoverableError;
     for (let i = 0; i < recoverableErrors.length; i++) {
       const recoverableError = recoverableErrors[i];
-      const componentStack = recoverableError.stack;
-      const digest = recoverableError.digest;
-      onRecoverableError(recoverableError.value, {componentStack, digest});
+      const errorInfo = makeErrorInfo(
+        recoverableError.digest,
+        recoverableError.stack,
+      );
+      onRecoverableError(recoverableError.value, errorInfo);
     }
   }
 
@@ -2689,6 +2691,33 @@ function commitRootImpl(
   return null;
 }
 
+function makeErrorInfo(digest: ?string, componentStack: ?string) {
+  if (__DEV__) {
+    const errorInfo = {
+      componentStack,
+      digest,
+    };
+    Object.defineProperty(errorInfo, 'digest', {
+      configurable: false,
+      enumerable: true,
+      get() {
+        console.error(
+          'You are accessing "digest" from the errorInfo object passed to onRecoverableError.' +
+            ' This property is deprecated and will be removed in a future version of React.' +
+            ' To access the digest of an Error look for this property on the Error instance itself.',
+        );
+        return digest;
+      },
+    });
+    return errorInfo;
+  } else {
+    return {
+      digest,
+      componentStack,
+    };
+  }
+}
+
 function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
   if (enableCache) {
     const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);

filename: packages/react-reconciler/src/ReactFiberWorkLoop.old.js
patch: @@ -2596,9 +2596,11 @@ function commitRootImpl(
     const onRecoverableError = root.onRecoverableError;
     for (let i = 0; i < recoverableErrors.length; i++) {
       const recoverableError = recoverableErrors[i];
-      const componentStack = recoverableError.stack;
-      const digest = recoverableError.digest;
-      onRecoverableError(recoverableError.value, {componentStack, digest});
+      const errorInfo = makeErrorInfo(
+        recoverableError.digest,
+        recoverableError.stack,
+      );
+      onRecoverableError(recoverableError.value, errorInfo);
     }
   }
 
@@ -2689,6 +2691,33 @@ function commitRootImpl(
   return null;
 }
 
+function makeErrorInfo(digest: ?string, componentStack: ?string) {
+  if (__DEV__) {
+    const errorInfo = {
+      componentStack,
+      digest,
+    };
+    Object.defineProperty(errorInfo, 'digest', {
+      configurable: false,
+      enumerable: true,
+      get() {
+        console.error(
+          'You are accessing "digest" from the errorInfo object passed to onRecoverableError.' +
+            ' This property is deprecated and will be removed in a future version of React.' +
+            ' To access the digest of an Error look for this property on the Error instance itself.',
+        );
+        return digest;
+      },
+    });
+    return errorInfo;
+  } else {
+    return {
+      digest,
+      componentStack,
+    };
+  }
+}
+
 function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
   if (enableCache) {
     const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);