Repository: facebook/react
Pull request: [DevTools] Throw error in console without interfering with logs

This PR uses `setTimeout` to throw an error outside the overridden console method so DevTools and React internal errors will still throw without interfering with logging

Changed files:
filename: packages/react-devtools-shared/src/__tests__/console-test.js
patch: @@ -6,20 +6,19 @@
  *
  * @flow
  */
+let React;
+let ReactDOM;
+let act;
+let fakeConsole;
+let legacyRender;
+let mockError;
+let mockInfo;
+let mockLog;
+let mockWarn;
+let patchConsole;
+let unpatchConsole;
 
 describe('console', () => {
-  let React;
-  let ReactDOM;
-  let act;
-  let fakeConsole;
-  let legacyRender;
-  let mockError;
-  let mockInfo;
-  let mockLog;
-  let mockWarn;
-  let patchConsole;
-  let unpatchConsole;
-
   beforeEach(() => {
     jest.resetModules();
 
@@ -553,3 +552,92 @@ describe('console', () => {
     expect(mockError.mock.calls[0][0]).toBe('error');
   });
 });
+
+describe('console error', () => {
+  beforeEach(() => {
+    jest.resetModules();
+
+    const Console = require('react-devtools-shared/src/backend/console');
+    patchConsole = Console.patch;
+    unpatchConsole = Console.unpatch;
+
+    // Patch a fake console so we can verify with tests below.
+    // Patching the real console is too complicated,
+    // because Jest itself has hooks into it as does our test env setup.
+    mockError = jest.fn();
+    mockInfo = jest.fn();
+    mockLog = jest.fn();
+    mockWarn = jest.fn();
+    fakeConsole = {
+      error: mockError,
+      info: mockInfo,
+      log: mockLog,
+      warn: mockWarn,
+    };
+
+    Console.dangerous_setTargetConsoleForTesting(fakeConsole);
+
+    // Note the Console module only patches once,
+    // so it's important to patch the test console before injection.
+    patchConsole({
+      appendComponentStack: true,
+      breakOnWarn: false,
+      showInlineWarningsAndErrors: false,
+      hideDoubleLogsInStrictLegacy: false,
+    });
+
+    const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
+    global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
+      internals.getIsStrictMode = () => {
+        throw Error('foo');
+      };
+      inject(internals);
+
+      Console.registerRenderer(internals);
+    };
+
+    React = require('react');
+    ReactDOM = require('react-dom');
+
+    const utils = require('./utils');
+    act = utils.act;
+    legacyRender = utils.legacyRender;
+  });
+
+  it('error in console log throws without interfering with logging', () => {
+    const container = document.createElement('div');
+    const root = ReactDOM.createRoot(container);
+
+    function App() {
+      fakeConsole.log('log');
+      fakeConsole.warn('warn');
+      fakeConsole.error('error');
+      return <div />;
+    }
+
+    patchConsole({
+      appendComponentStack: true,
+      breakOnWarn: false,
+      showInlineWarningsAndErrors: false,
+      hideConsoleLogsInStrictMode: false,
+    });
+
+    expect(() => {
+      act(() => {
+        root.render(<App />);
+      });
+    }).toThrowError('foo');
+
+    expect(mockLog).toHaveBeenCalledTimes(1);
+    expect(mockLog.mock.calls[0]).toHaveLength(1);
+    expect(mockLog.mock.calls[0][0]).toBe('log');
+
+    expect(mockWarn).toHaveBeenCalledTimes(1);
+    expect(mockWarn.mock.calls[0]).toHaveLength(1);
+    expect(mockWarn.mock.calls[0][0]).toBe('warn');
+
+    expect(mockError).toHaveBeenCalledTimes(1);
+    expect(mockError.mock.calls[0]).toHaveLength(1);
+    expect(mockError.mock.calls[0][0]).toBe('error');
+  });
+});

filename: packages/react-devtools-shared/src/backend/console.js
patch: @@ -222,6 +222,9 @@ export function patch({
               }
             } catch (error) {
               // Don't let a DevTools or React internal error interfere with logging.
+              setTimeout(() => {
+                throw error;
+              }, 0);
             } finally {
               break;
             }