react: Bug: Error in useEffect is caught in ErrorBoundary, but still logs uncaught error to console in tests

React version: 16.13.1

Steps To Reproduce

  1. Create a component with a useEffect hook
  2. Render the component in an error boundary using react-test-renderer
  3. Throw an error synchronously in the effect callback (i.e. not in a promise callback)
  4. Observe the error in the console

Link to code example: https://github.com/mpeyper/error-boundary-error-repro

The current behavior

When you first clone the repo, all the tests are being skipped. This is to reduce the noise when observing the output. Please remove the .skip from the tests to see the output.

Note that I have also used react-error-boundary for creating the error boundaries in the example repo to simplify the setup, but if you believe this is interfering with test, I’m happy to hand roll an error boundary instead.

Given the following two components, the way in which the error is being handled is inconsistent when using react-test-renderer vs. react-dom (via @testing-library/react in my example):

export function HasErrorInRender() {
  throw Error("This error was expected")
}

export function HasErrorInEffect() {
  useEffect(() => {
    throw Error("This error was expected")
  })

  return <p>This component has an error in an effect</p>
}

When rendering with react-dom, both the following tests pass, and produce the same output in the console:

describe('@testing-library/react', () => {
  test('should catch error in render', () => {
    let err = null
    function Fallback({ error }) {
      err = error
      return <p>An error was thrown</p>
    }

    render((
      <ErrorBoundary FallbackComponent={Fallback}>
        <HasErrorInRender />
      </ErrorBoundary>
    ))

    expect(err).toEqual(Error("This error was expected"))
  })

  test('should catch error in effect', () => {
    let err = null
    function Fallback({ error }) {
      err = error
      return <p>An error was thrown</p>
    }

    render((
      <ErrorBoundary FallbackComponent={Fallback}>
        <HasErrorInEffect />
      </ErrorBoundary>
    ))

    expect(err).toEqual(Error("This error was expected"))
  })
})

The output they produce is:

  console.error node_modules/jsdom/lib/jsdom/virtual-console.js:29
    Error: Uncaught [Error: This error was expected]
        at reportException (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:24)
        at invokeEventListeners (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:209:9)
        at HTMLUnknownElementImpl._dispatch (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
        at HTMLUnknownElementImpl.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
        at HTMLUnknownElementImpl.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
        at HTMLUnknownElement.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
        at Object.invokeGuardedCallbackDev (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:237:16)
        at invokeGuardedCallback (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:292:31)
        at beginWork$1 (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:23203:7)
        at performUnitOfWork (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:22157:12) Error: This error was expected
        at HasErrorInRender (/<REDACTED>/error-boundary-error-repro/src/HasError.js:4:9)
        at renderWithHooks (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:14803:18)
        at mountIndeterminateComponent (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:17482:13)
        at beginWork (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:18596:16)
        at HTMLUnknownElement.callCallback (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:188:14)
        at invokeEventListeners (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:193:27)
        at HTMLUnknownElementImpl._dispatch (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
        at HTMLUnknownElementImpl.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
        at HTMLUnknownElementImpl.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
        at HTMLUnknownElement.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
        at Object.invokeGuardedCallbackDev (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:237:16)
        at invokeGuardedCallback (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:292:31)
        at beginWork$1 (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:23203:7)
        at performUnitOfWork (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:22157:12)
        at workLoopSync (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:22130:22)
        at performSyncWorkOnRoot (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:21756:9)
        at scheduleUpdateOnFiber (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:21188:7)
        at updateContainer (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:24373:3)
        at /<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:24758:7
        at unbatchedUpdates (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:21903:12)
        at legacyRenderSubtreeIntoContainer (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:24757:5)
        at Object.render (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:24840:10)
        at /<REDACTED>/error-boundary-error-repro/node_modules/@testing-library/react/dist/pure.js:86:25
        at batchedUpdates$1 (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom.development.js:21856:12)
        at act (/<REDACTED>/error-boundary-error-repro/node_modules/react-dom/cjs/react-dom-test-utils.development.js:929:14)
        at render (/<REDACTED>/error-boundary-error-repro/node_modules/@testing-library/react/dist/pure.js:82:26)
        at Object.<anonymous> (/<REDACTED>/error-boundary-error-repro/src/HasError.test.js:15:5)
        at Object.asyncJestTest (/<REDACTED>/error-boundary-error-repro/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
        at /<REDACTED>/error-boundary-error-repro/node_modules/jest-jasmine2/build/queueRunner.js:43:12
        at new Promise (<anonymous>)
        at mapper (/<REDACTED>/error-boundary-error-repro/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
        at /<REDACTED>/error-boundary-error-repro/node_modules/jest-jasmine2/build/queueRunner.js:73:41
        at processTicksAndRejections (internal/process/task_queues.js:97:5)

  console.error node_modules/react-dom/cjs/react-dom.development.js:19527
    The above error occurred in the <HasErrorInRender> component:
        in HasErrorInRender (at HasError.test.js:17)
        in ErrorBoundary (at HasError.test.js:16)
    
    React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.

This is somewhat expected when using react-dom and reflects the output one would see in the browser console if the some components were rendered in an app (you can run npm start in the example repo if you would like to observe this).

However, where things start to get a bit strange is when the renderer is replaced with react-test-renderer and the same tests are run:

describe('react-test-renderer', () => {
  test('should catch error in render', () => {
    let err = null
    function Fallback({ error }) {
      err = error
      return <p>An error was thrown</p>
    }

    act(() => {
      create((
        <ErrorBoundary FallbackComponent={Fallback}>
          <HasErrorInRender />
        </ErrorBoundary>
      ))
    })

    expect(err).toEqual(Error("This error was expected"))
  })

  test('should catch error in effect', () => {
    let err = null
    function Fallback({ error }) {
      err = error
      return <p>An error was thrown</p>
    }

    act(() => {
      create((
        <ErrorBoundary FallbackComponent={Fallback}>
          <HasErrorInEffect />
        </ErrorBoundary>
      ))
    })

    expect(err).toEqual(Error("This error was expected"))
  })
})

Again both tests here do pass, but the output they produce is not the same. When the first test (error in the render function) is run, it only produces the following output:

  console.error node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10141
    The above error occurred in the <HasErrorInRender> component:
        in HasErrorInRender (at HasError.test.js:52)
        in ErrorBoundary (at HasError.test.js:51)
    
    React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.

As you can see, the frustratingly difficult to suppress error log from the error boundary is present, but the long stack trace from the uncaught error that is present in the react-dom output is not.

When the second test (error in the useEffect callback) is run, the output is:

  console.error node_modules/jsdom/lib/jsdom/virtual-console.js:29
    Error: Uncaught [Error: This error was expected]
        at reportException (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:24)
        at invokeEventListeners (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:209:9)
        at HTMLUnknownElementImpl._dispatch (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
        at HTMLUnknownElementImpl.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
        at HTMLUnknownElementImpl.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
        at HTMLUnknownElement.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
        at Object.invokeGuardedCallbackDev (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10021:16)
        at invokeGuardedCallback (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10073:31)
        at flushPassiveEffectsImpl (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13345:9)
        at unstable_runWithPriority (/<REDACTED>/error-boundary-error-repro/node_modules/scheduler/cjs/scheduler.development.js:653:12) Error: This error was expected
        at /<REDACTED>/error-boundary-error-repro/src/HasError.js:9:11
        at commitHookEffectListMount (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10345:26)
        at commitPassiveHookEffects (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10383:11)
        at HTMLUnknownElement.callCallback (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:9972:14)
        at invokeEventListeners (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:193:27)
        at HTMLUnknownElementImpl._dispatch (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
        at HTMLUnknownElementImpl.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
        at HTMLUnknownElementImpl.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
        at HTMLUnknownElement.dispatchEvent (/<REDACTED>/error-boundary-error-repro/node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
        at Object.invokeGuardedCallbackDev (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10021:16)
        at invokeGuardedCallback (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10073:31)
        at flushPassiveEffectsImpl (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13345:9)
        at unstable_runWithPriority (/<REDACTED>/error-boundary-error-repro/node_modules/scheduler/cjs/scheduler.development.js:653:12)
        at runWithPriority (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:1775:10)
        at flushPassiveEffects (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13312:12)
        at Object.<anonymous>.flushWork (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:14883:10)
        at act (/<REDACTED>/error-boundary-error-repro/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15001:9)
        at Object.<anonymous> (/<REDACTED>/error-boundary-error-repro/src/HasError.test.js:67:5)
        at Object.asyncJestTest (/<REDACTED>/error-boundary-error-repro/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
        at /<REDACTED>/error-boundary-error-repro/node_modules/jest-jasmine2/build/queueRunner.js:43:12
        at new Promise (<anonymous>)
        at mapper (/<REDACTED>/error-boundary-error-repro/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
        at /<REDACTED>/error-boundary-error-repro/node_modules/jest-jasmine2/build/queueRunner.js:73:41
        at processTicksAndRejections (internal/process/task_queues.js:97:5)

  console.error node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10141
    The above error occurred in the <HasErrorInEffect> component:
        in HasErrorInEffect (at HasError.test.js:70)
        in ErrorBoundary (at HasError.test.js:69)
    
    React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.

Now, the uncaught error message is back, which is not what I would have expected. Even more confusingly, when inspecting the stacktrace of the uncaught error, it has references to jsdom which I was of the belief was not a dependency of react-test-renderer. I suspect that there is some jest and/or jsdom trickery going on to report the uncaught error, rather than react-test-renderer using it in some way, but I’m not familiar enough with any of them to know for certain.

The part that’s has me the most perplexed is how the error boundary can intercept the error to pass into it’s handler callbacks (surfaced in my example in react-error-boundary’s FallbackComponent) without catching the error, unless it is throwing it again after catching it, but then both tests would be producing the uncaught error output, right?

The expected behavior

My expected (and preferred) behaviour here would be for the the react-test-renderer test to only produce the error boundary error log and not have any additional uncaught error output.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 4
  • Comments: 15

Most upvoted comments

I’m having the same issue. I implemented a top-level ErrorBoundary component just as described in the docs, and when I throw an error during rendering, the component that the ErrorBoundary should render is actually rendered, but for some reason the error is still logged as an Uncaught Error. Not sure if this is the desired behavior.

Screen Shot 2022-06-07 at 12 37 55 PM