mirror of
https://github.com/facebook/react.git
synced 2026-02-24 12:43:00 +00:00
We basically have four kinds of recoverable errors: - Hydration mismatches. - Server errored but client didn't. - Hydration render errored but client render didn't (in Root or Suspense boundary). - Concurrent render errored but synchronous render didn't. For the first three we log an additional error that the root or Suspense boundary didn't error. This provides some context about what happened. However, the problem is that for hydration mismatches that's unnecessary extra context that is confusing. We also don't log any additional context for concurrent render errors that could recover. This used to be the only recoverable error so it didn't need extra context but now we need to distinguish them. When we log these to `reportError` it's confusing to just see the error because you didn't see anything error on the page. It's also hard to group them together as one. In this PR, I remove the unnecessary context for hydration mismatches. For hydration and concurrent errors, I now wrap them in an error that describes that what happened but then use the new `cause` field to link the original error so we can keep that as the cause. The error that happened was that hydration client rendered or you deopted to sync render, the cause of that error is some other error. For server errors, we control the Error object so I already had added some context to that error object's message. Since we hide the message in prod, it's nice not to have the raw message in DEV neither. We could potentially split these into two errors for parity though.
169 lines
4.1 KiB
JavaScript
169 lines
4.1 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
/* eslint-disable quotes */
|
|
'use strict';
|
|
|
|
let babel = require('@babel/core');
|
|
let devExpressionWithCodes = require('../transform-error-messages');
|
|
|
|
function transform(input, options = {}) {
|
|
return babel.transform(input, {
|
|
plugins: [[devExpressionWithCodes, options]],
|
|
}).code;
|
|
}
|
|
|
|
let oldEnv;
|
|
|
|
describe('error transform', () => {
|
|
beforeEach(() => {
|
|
oldEnv = process.env.NODE_ENV;
|
|
process.env.NODE_ENV = '';
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env.NODE_ENV = oldEnv;
|
|
});
|
|
|
|
it('should replace error constructors', () => {
|
|
expect(
|
|
transform(`
|
|
new Error('Do not override existing functions.');
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('should replace error constructors (no new)', () => {
|
|
expect(
|
|
transform(`
|
|
Error('Do not override existing functions.');
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it("should output FIXME for errors that don't have a matching error code", () => {
|
|
expect(
|
|
transform(`
|
|
Error('This is not a real error message.');
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it(
|
|
"should output FIXME for errors that don't have a matching error " +
|
|
'code, unless opted out with a comment',
|
|
() => {
|
|
// TODO: Since this only detects one of many ways to disable a lint
|
|
// rule, we should instead search for a custom directive (like
|
|
// no-minify-errors) instead of ESLint. Will need to update our lint
|
|
// rule to recognize the same directive.
|
|
expect(
|
|
transform(`
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
Error('This is not a real error message.');
|
|
`)
|
|
).toMatchSnapshot();
|
|
}
|
|
);
|
|
|
|
it('should not touch other calls or new expressions', () => {
|
|
expect(
|
|
transform(`
|
|
new NotAnError();
|
|
NotAnError();
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('should support interpolating arguments with template strings', () => {
|
|
expect(
|
|
transform(`
|
|
new Error(\`Expected \${foo} target to be an array; got \${bar}\`);
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('should support interpolating arguments with concatenation', () => {
|
|
expect(
|
|
transform(`
|
|
new Error('Expected ' + foo + ' target to be an array; got ' + bar);
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('should support error constructors with concatenated messages', () => {
|
|
expect(
|
|
transform(`
|
|
new Error(\`Expected \${foo} target to \` + \`be an array; got \${bar}\`);
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('handles escaped backticks in template string', () => {
|
|
expect(
|
|
transform(`
|
|
new Error(\`Expected \\\`\$\{listener\}\\\` listener to be a function, instead got a value of \\\`\$\{type\}\\\` type.\`);
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('handles ignoring errors that are comment-excluded inside ternary expressions', () => {
|
|
expect(
|
|
transform(`
|
|
let val = someBool
|
|
? //eslint-disable-next-line react-internal/prod-error-codes
|
|
new Error('foo')
|
|
: someOtherBool
|
|
? new Error('bar')
|
|
: //eslint-disable-next-line react-internal/prod-error-codes
|
|
new Error('baz');
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('handles ignoring errors that are comment-excluded outside ternary expressions', () => {
|
|
expect(
|
|
transform(`
|
|
//eslint-disable-next-line react-internal/prod-error-codes
|
|
let val = someBool
|
|
? new Error('foo')
|
|
: someOtherBool
|
|
? new Error('bar')
|
|
: new Error('baz');
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('handles deeply nested expressions', () => {
|
|
expect(
|
|
transform(`
|
|
let val =
|
|
(a,
|
|
(b,
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
new Error('foo')));
|
|
`)
|
|
).toMatchSnapshot();
|
|
|
|
expect(
|
|
transform(`
|
|
let val =
|
|
(a,
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
(b, new Error('foo')));
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
|
|
it('should support extra arguments to error constructor', () => {
|
|
expect(
|
|
transform(`
|
|
new Error(\`Expected \${foo} target to \` + \`be an array; got \${bar}\`, {cause: error});
|
|
`)
|
|
).toMatchSnapshot();
|
|
});
|
|
});
|