(This is a repost of https://github.com/facebook/jest/issues/4597 by @erikras.)
Do you want to request a feature or report a bug?
Somewhere in between?
What is the current behavior?
When I'm running tests on my library, there are some behaviors that I want to test _do_ throw an error. These currently result in:
Consider adding an error boundary to your tree to customize error handling behavior.
You can learn more about error boundaries at https://fb.me/react-error-boundaries.
...being output to the console. This error is great in an application, but not so great for a library test.
What is the expected behavior?
It would be great if I could do something like:
expect(() => {
TestUtils.renderIntoDocument(<DoSomething naughty/>)
})
.toThrow(/Bad developer!/)
.andCatch() // <---- prevents React 16 error boundary warning
Please provide your exact Jest configuration and mention your Jest, node, yarn/npm version and operating system.
[email protected]
[email protected]
[email protected]
See also
cc @acdlite @sebmarkbage @bvaughn
This error is great in an application, but not so great for a library test.
One way to prevent these unwanted messages from appearing in tests is:
spyOn(console, 'error'); // In tests that you expect errors
If this is too onerous you could also do it globally in a Jest setup file.
I don't think _we_ saw these warnings. But we probably mocked something in Jest at some point and that's why.
Regarding @gaearon's comment on the original issue, I believe we don't see them because we do a mix of spying on console.error
and mocking ReactFiberErrorLogger
.
Unfortunately mocking out ReactFiberErrorLogger
isn't really recommended outside of the context of the React project. We should come up with a cleaner story for this in order to simplify external testing.
i've been running into similar issues in mocha where error behavior under test is being hidden by this warning.
my issue was made somewhat worse because of the fact that i override console.error
and console.warn
in my tests to ensure that react warnings cause test failures. this caused those errors to be caught by this error boundary log even earlier in some cases. removing these overrides allowed me to see further error output, but resolving the newly revealed output brought me back to the boundary logs again.
if there is anything i could help with as far as insight from testing with mocha related to this issue, i'm happy to.
i've been running into similar issues in mocha where error behavior under test is being hidden by this warning.
Can you clarify what you mean by this?
When you see this warning but your test doesn't fail, it means your code is throwing somewhere but then this error gets caught and never reaches your test's stack frame. Unless you're intentionally testing error cases, this indicates a bug in your code. But this warning itself doesn't "hide" the original error. If we removed the warning your bug would be completely invisible (since something catches it). The warning surfaces it.
I see two separate issues here:
"The above error occurred" message is not helpful in a test environment because we're not showing the actual thrown. We're hoping the browser will show it, but jsdom isn't a browser. Maybe there's some sort of feature detection we could do to determine whether to show the full message?
Some people might want to disable error reporting in tests altogether. I think in general this is a bad idea. But there might be legitimate reasons to do this. https://github.com/facebook/react/pull/11636 already provides an escape hatch for this in 16.2+.
Sorry for the delayed response on this.
Can you clarify what you mean by this?
I stub console.error
and console.warn
so that any React warnings need to be fixed in order for our test suite to pass.
console.error = err => { throw new Error(err); };
console.warn = warning => { throw new Error(warning); };
with those stubs in place, if a warning is triggered, it is elevated to an error. that error then gets picked up by the error boundary suggestion, hiding the actual warning. for example:
Error: Error: The above error occurred in the
component:
...
...
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.
...
...
instead of
Warning: Unknown event handler property
onLeftIconButtonTouchTap
. It will be ignored.
...
...
hopefully that helps explain that situation a bit better
Yes, this is the first issue I described in https://github.com/facebook/react/issues/11098#issuecomment-347625796. I agree we need to fix it but it’s not obvious to me how. The browser shows errors that are thrown like this but jsdom doesn’t. Why? Should we file an issue with jsdom?
The browser shows errors that are thrown like this but jsdom doesn’t. Why? Should we file an issue with jsdom?
Answering my own question: jsdom does show these errors now. Updating to Jest 22 surfaced them in our test suite. So even if we ignored them in React, they would still show up in your tests as logs.
It turns out that you can e.preventDefault()
in the error
event handler for the window
object to prevent the browser (or jsdom) from logging the error. I didn’t know that.
I think it would make sense to me if React did the same.
Here is a minimal patch to React that would do that:
```diff
--- a/packages/shared/invokeGuardedCallback.js
+++ b/packages/shared/invokeGuardedCallback.js
@@ -125,6 +125,7 @@ if (__DEV__) {
// Use this to track whether the error event is ever called.
let didSetError = false;
let isCrossOriginError = false;
function onError(event) {
error = event.error;
@@ -132,6 +133,9 @@ if (__DEV__) {
if (error === null && event.colno === 0 && event.lineno === 0) {
isCrossOriginError = true;
}
// Create a fake event type.
@@ -172,6 +176,9 @@ if (__DEV__) {
this._hasCaughtError = false;
this._caughtError = null;
}
It's not great though because it relies on setting an undocumented field (suppressReactErrorLogging
) on the error object, thereby mutating it. I only added this field in https://github.com/facebook/react/pull/11636 because I didn't know that there is a preventDefault()
convention for preventing logging of intentionally swallowed errors.
What do you think about this course of action:
suppressReactErrorLogging
property.e.defaultPrevented
in our error
event handler in invokeGuardedCallback.js
._hasCaughtError
and _caughtError
.ReactFiberScheduler
to not log the "muted" errors according to this new flag.error
handler in our tests that calls e.preventDefault()
(and thus silences the logs).I’m probably not motivated enough to follow through with this but if somebody else wants to take it, I’m happy to discuss.
I like the idea of replacing suppressReactErrorLogging
with e. defaultPrevented
đź‘Ť
Tagging as a good issue to look into. Not promising we'll merge a solution, but worth investigating.
Proposed implementation plan: https://github.com/facebook/react/issues/11098#issuecomment-355032539.
Can I take this issue for my first contribution?
Sure.
Hi,
I am able to re-produce the issue. (code).
Output
console.error node_modules/react-dom/cjs/react-dom.development.js:9747
The above error occurred in the <Card> component:
in Card (at Card.test.js:16)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.
and later by adding the suppressReactErrorLogging
in the tests the error boundary warning is gone. (code)
So approach will be
suppressReactErrorLogging
by following the commentQuestions
suppressReactErrorLogging
property: I am not sure what code changes are required to remove custom property! what I am thinking is to remove (ReactFiberErrorLogger) and (ReactFiberScheduler).So am I going right?
This sounds like a good first step but then you'll need to use event.defaultPrevented
in invokeGuardedCallbackDev
to decided whether the error should be suppressed, and expose this information to the scheduler. In my comment above I outlined a strategy for doing this.
This is also interesting for react-native customers, and I'm not sure if event.preventDefault
/ invokeGuardedCallbackDev
apply in that situation.
A temporary hack until there is some kind of solution. Prevents the errors from printing using console.error
.
beforeEach(() => {
jest.spyOn(console, 'error')
global.console.error.mockImplementation(() => {})
})
afterEach(() => {
global.console.error.mockRestore()
})
test('this should throw and pass and not log the error' () => {
expect(() => {
TestUtils.renderIntoDocument(<DoSomething naughty/>)
})
.toThrow(/Bad developer!/)
})
You will still see console.errors in other test modules.
Is this really hacky thing still the solution? :/
This is still causing tons of noise in our unit tests. Anyone working on this?
@craigkovatch No. If someone was working on this, they would probably post in this thread. You're welcome to propose a solution — I outlined some ideas above but maybe they don't make much sense? It's been a while since I looked at this, and somebody with a fresh perspective would probably make more progress.
I think I might have a plan for this. Will try to send a PR soon.
Thanks Dan! And sorry for my lack of response on your proposal, I'm not versed in the internals of React so didn't feel competent to comment.
All right. I have merged a fix in https://github.com/facebook/react/pull/13384 which I think strikes a reasonable balance between giving you control over the warning noise and preventing accidentally swallowed errors.
Specifically, in the next React release (likely 16.4.3), we won't log the extra message (The above error occurred ...
) if these three conditions are all true:
event.preventDefault()
for that error in a custom error
event handlerLet me unpack what this means, and how you can adjust your tests.
Starting with React 16.4.3 (not out yet), you will be able to suppress rendering errors in tests by using a special helper (or a variation of the same technique). I put the example here: https://gist.github.com/gaearon/adf9d5500e11a4e7b2c6f7ebf994fe56.
It won't generate any warnings for intentionally thrown errors when you use ReactDOM in development mode in a jsdom environment. It will, however, fail the tests for unintentionally thrown errors, even if those were silenced by nested error boundaries.
If this helper is not sufficient for some reason (e.g. if you're using test renderer instead of ReactDOM) please keep mocking console.error
. There's nothing wrong about that approach.
Now, if you're curious, let me guide you through why it works this way.
Consider this example:
const React = require('react');
const ReactDOM = require('react-dom');
function Darth() {
throw new Error('I am your father')
}
it('errors', () => {
const div = document.createElement('div');
expect(() => {
ReactDOM.render(<Darth />, div);
}).toThrow('father');
});
Before this change and with an older version of jsdom (the one that ships in the currently stable Create React App), the output looks like this:
PASS src/App.test.js
âś“ errors
console.error node_modules/react-dom/cjs/react-dom.development.js:14227
The above error occurred in the <Darth> component:
in Darth (at App.test.js:12)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.
Not too bad although the warning is a bit annoying. However, still, before this change, it gets worse if you update to a recent version of jsdom (through a Jest update):
PASS src/App.test.js
âś“ errors
console.error node_modules/jsdom/lib/jsdom/virtual-console.js:29
Error: Uncaught [Error: I am your father]
at reportException (/Users/gaearon/p/testj/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:24)
<...>
at promise.then (/Users/gaearon/p/testj/node_modules/jest-jasmine2/build/queue_runner.js:87:41)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
console.error node_modules/react-dom/cjs/react-dom.development.js:14227
The above error occurred in the <Darth> component:
in Darth
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.
You can see that the test is still passing but there are two annoying warnings. Let's dig into why we see them.
We see two warnings:
Error: Uncaught [...]
The above error occurred ...
The first one is coming from jsdom. The second one is coming from React.
The new jsdom warning is great and mimics what browsers do. It allows us report errors even if they were accidentally swallowed by the component code — for example:
fetchSomething()
.then((data) => this.setState({ data })) // oops! errors from this render are swallowed
.catch(err => this.setState({ err }))
Before React 16, this error would be swallowed, but React 16 still surfaces it to the user. This is a feature, not a bug. It's great that jsdom does it too now, and it's a good default for your tests — especially for application tests. But it can be annoying for libraries.
What about the cases where you don't want to see it? In the browsers, you can opt out by calling preventDefault()
in a custom error
event handler on the window
object.
function onError(event) {
// Note: this will swallow reports about unhandled errors!
// Use with extreme caution.
event.preventDefault();
}
window.addEventListener('error', onError);
This should only be used with extreme caution — it's easy to accidentally mute information about important warnings. Here's a more granular approach we can use in tests that intentionally throw errors:
const React = require('react');
const ReactDOM = require('react-dom');
function Darth() {
throw new Error('I am your father')
}
let expectedErrors = 0;
let actualErrors = 0;
function onError(e) {
e.preventDefault();
actualErrors++;
}
beforeEach(() => {
expectedErrors = 0;
actualErrors = 0;
window.addEventListener('error', onError);
});
afterEach(() => {
window.removeEventListener('error', onError);
expect(actualErrors).toBe(expectedErrors);
expectedErrors = 0;
});
it('errors', () => {
expectedErrors = 1; // Remember only one error was expected
const div = document.createElement('div');
expect(() => {
ReactDOM.render(<Darth />, div);
}).toThrow('father');
});
Note how I'm using preventDefault
to silence the expected warnings. However, if there's a deeper component that has thrown an error which was accidentally caught an silenced, your test would still fail because you'd get an extra unexpected error.
If you use this approach, make sure that every addEventListener
call has a matching removeEventListener
call. I intentionally put them in beforeEach
and afterEach
so that even if a test fails, the calls still always match up. An alternative if you don't like beforeEach
is to use try / finally
blocks.
With this above test suite, we've effectively silenced the noisy jsdom warning. But we still see the component stack from React:
PASS src/App.test.js
âś“ errors
console.error node_modules/react-dom/cjs/react-dom.development.js:14227
The above error occurred in the <Darth> component:
in Darth
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.
Let's see how we can fix this.
Note this section will only work in the next React release — most likely, 16.4.3. I'm still writing it up here for future reference.
The component stack is important to print because it helps locate the source of the error. With https://github.com/facebook/react/pull/13384, I changed the logic so that we won't print the stack if the error itself was silenced (as described above) and the error was handled by an error boundary. In the above example we're not using an error boundary, and that's why we see the extra stack.
We can fix this by extracting a method called expectRenderError
that uses an error boundary:
const React = require('react');
const ReactDOM = require('react-dom');
function expectRenderError(element, expectedError) {
// Noop error boundary for testing.
class TestBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { didError: false };
}
componentDidCatch(err) {
this.setState({ didError: true });
}
render() {
return this.state.didError ? null : this.props.children;
}
}
// Record all errors.
let topLevelErrors = [];
function handleTopLevelError(event) {
topLevelErrors.push(event.error);
// Prevent logging
event.preventDefault();
}
const div = document.createElement('div');
window.addEventListener('error', handleTopLevelError);
try {
ReactDOM.render(
<TestBoundary>
{element}
</TestBoundary>,
div
);
} finally {
window.removeEventListener('error', handleTopLevelError);
}
expect(topLevelErrors.length).toBe(1);
expect(topLevelErrors[0].message).toContain(expectedError);
}
I'm not suggesting to copy and paste this helper into every test. It's probably best that you define it once in your project, or even put it on npm. With this approach, our test can be very simple again:
function Darth() {
throw new Error('I am your father')
}
it('errors', () => {
expectRenderError(
<Darth />,
'father'
);
});
As I mentioned in the beginning, I put this helper here: https://gist.github.com/gaearon/adf9d5500e11a4e7b2c6f7ebf994fe56.
Feel free to republish it on npm, turn it into a Jest matcher, etc.
Hope this helps! I'm sorry if it's disappointing we don't just disable this warning. But I hope you can see the rationale: we think it's extremely important to prevent app developers from accidentally swallowing errors. Even if it comes with the cost of some additional bookkeeping for library developers who want to make assertions about thrown errors.
Finally, if this approach doesn't work out for you I'd like to clarify there's nothing bad about mocking console.error
either — we do it all the time in the React suite (and have written custom matchers for ourselves to assist with that). Perhaps you'd want to do something similar if you have a lot of tests around throwing errors from React components.
Cheers!
If it helps, what we do in React's own test suite is much simpler although it can also potentially filter out some valid warnings.
Hey @gaearon! Thank you for that patch in React 16.5. I tried it in our project and it unfortunately failed. I constructed a minimal reproduction of the issue.
Importing enzyme-adapter-react-16
in the setup of the test leads to a test failure with the proposed solution. It seems that the event listener is not triggered when the adapter is present.
Do you know of a way around this?
This issue was solved for me. When using enzyme
, simulateError
does also prevent the error log in the console https://github.com/airbnb/enzyme/issues/1826#issuecomment-423082090 đź‘Ť
If it can spare others some burden, I gave the expectRenderError
trick a try but I still get the following message:
React will try to recreate this component tree from scratch using the error boundary you provided, TestBoundary.
just use simulateError
method。 in my case it works fine.
https://airbnb.io/enzyme/docs/api/ReactWrapper/simulateError.html
If anyone's struggling with how to test an error boundary with React Testing Library, I hope this helps:
it('renders "Something went wrong." when an error is thrown', () => {
const spy = jest.spyOn(console, 'error')
spy.mockImplementation(() => {})
const Throw = () => {
throw new Error('bad')
}
const { getByText } = render(
<ErrorBoundary>
<Throw />
</ErrorBoundary>,
)
expect(getByText('Something went wrong.')).toBeDefined()
spy.mockRestore()
})
This is the dumbest error message I have ever seen. I don't want you to tell me about error boundaries, I want you to be quiet. SMH
It's a shame this has been closed as it does provide an awful lot of noise in tests where an exception is expected to be thrown. We shouldn't have to be mocking the console to cut this noise out.
The following error still shows for me:
console.error ../node_modules/react-dom/cjs/react-dom.development.js:530
Warning: ErrorCatcher: Error boundaries should implement getDerivedStateFromError(). In that method, return a state update to display an error message or fallback UI.
Docs say that this is optional but jest is warning as if it was mandatory
I was only able to get rid of it by mocking the console
Most helpful comment
If anyone's struggling with how to test an error boundary with React Testing Library, I hope this helps: