Construct Error at invariant call site for clearer stack traces (#15877)
Moti Zilberman committed
Jun 18, 2019 at 19:38 UTC
98454371a995d73a6cb2f3f4770c13945fb78ea4
6 files changed
+80
-37
packages/shared/ReactError.js
+2
-3
@@ -6,12 +6,11 @@
6
*
7
*/
8
9
-// Do not require this module directly! Use a normal error constructor with
9
+// Do not require this module directly! Use normal `invariant` calls with
10
// template literal strings. The messages will be converted to ReactError during
11
// build, and in production they will be minified.
12
13
-function ReactError(message) {
14
- const error = new Error(message);
13
+function ReactError(error) {
14
error.name = 'Invariant Violation';
15
return error;
16
}
packages/shared/ReactErrorProd.js
+7
-6
@@ -6,20 +6,21 @@
6
*
7
*/
8
9
-// Do not require this module directly! Use a normal error constructor with
9
+// Do not require this module directly! Use normal `invariant` calls with
10
// template literal strings. The messages will be converted to ReactError during
11
// build, and in production they will be minified.
12
13
-function ReactErrorProd(code) {
13
+function ReactErrorProd(error) {
14
+ const code = error.message;
15
let url = 'https://reactjs.org/docs/error-decoder.html?invariant=' + code;
16
for (let i = 1; i < arguments.length; i++) {
17
url += '&args[]=' + encodeURIComponent(arguments[i]);
18
}
18
- return new Error(
19
+ error.message =
20
`Minified React error #${code}; visit ${url} for the full message or ` +
20
- 'use the non-minified dev environment for full errors and additional ' +
21
- 'helpful warnings. ',
22
- );
21
+ 'use the non-minified dev environment for full errors and additional ' +
22
+ 'helpful warnings. ';
23
+ return error;
24
}
25
26
export default ReactErrorProd;
packages/shared/__tests__/ReactErrorProd-test.internal.js
+3
-3
@@ -36,7 +36,7 @@ describe('ReactErrorProd', () => {
36
37
it('should throw with the correct number of `%s`s in the URL', () => {
38
expect(function() {
39
- throw ReactErrorProd(124, 'foo', 'bar');
39
+ throw ReactErrorProd(Error(124), 'foo', 'bar');
40
}).toThrowError(
41
'Minified React error #124; visit ' +
42
'https://reactjs.org/docs/error-decoder.html?invariant=124&args[]=foo&args[]=bar' +
@@ -45,7 +45,7 @@ describe('ReactErrorProd', () => {
45
);
46
47
expect(function() {
48
- throw ReactErrorProd(20);
48
+ throw ReactErrorProd(Error(20));
49
}).toThrowError(
50
'Minified React error #20; visit ' +
51
'https://reactjs.org/docs/error-decoder.html?invariant=20' +
@@ -54,7 +54,7 @@ describe('ReactErrorProd', () => {
54
);
55
56
expect(function() {
57
- throw ReactErrorProd(77, '<div>', '&?bar');
57
+ throw ReactErrorProd(Error(77), '<div>', '&?bar');
58
}).toThrowError(
59
'Minified React error #77; visit ' +
60
'https://reactjs.org/docs/error-decoder.html?invariant=77&args[]=%3Cdiv%3E&args[]=%26%3Fbar' +
scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap
+13
-13
@@ -6,7 +6,7 @@ exports[`error transform should correctly transform invariants that are not in t
6
import invariant from 'shared/invariant';
7
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/(function () {
8
if (!condition) {
9
- throw _ReactError(\`This is not a real error message.\`);
9
+ throw _ReactError(Error(\`This is not a real error message.\`));
10
}
11
})();"
12
`;
@@ -17,7 +17,7 @@ exports[`error transform should handle escaped characters 1`] = `
17
import invariant from 'shared/invariant';
18
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/(function () {
19
if (!condition) {
20
- throw _ReactError(\`What's up?\`);
20
+ throw _ReactError(Error(\`What's up?\`));
21
}
22
})();"
23
`;
@@ -30,18 +30,18 @@ import invariant from 'shared/invariant';
30
(function () {
31
if (!condition) {
32
if (__DEV__) {
33
- throw _ReactError(\`Do not override existing functions.\`);
33
+ throw _ReactError(Error(\`Do not override existing functions.\`));
34
} else {
35
- throw _ReactErrorProd(16);
35
+ throw _ReactErrorProd(Error(16));
36
}
37
}
38
})();
39
(function () {
40
if (!condition) {
41
if (__DEV__) {
42
- throw _ReactError(\`Do not override existing functions.\`);
42
+ throw _ReactError(Error(\`Do not override existing functions.\`));
43
} else {
44
- throw _ReactErrorProd(16);
44
+ throw _ReactErrorProd(Error(16));
45
}
46
}
47
})();"
@@ -55,9 +55,9 @@ import invariant from 'shared/invariant';
55
(function () {
56
if (!condition) {
57
if (__DEV__) {
58
- throw _ReactError(\`Do not override existing functions.\`);
58
+ throw _ReactError(Error(\`Do not override existing functions.\`));
59
} else {
60
- throw _ReactErrorProd(16);
60
+ throw _ReactErrorProd(Error(16));
61
}
62
}
63
})();"
@@ -71,9 +71,9 @@ import invariant from 'shared/invariant';
71
(function () {
72
if (!condition) {
73
if (__DEV__) {
74
- throw _ReactError(\`Expected a component class, got \${Foo}.\${Bar}\`);
74
+ throw _ReactError(Error(\`Expected a component class, got \${Foo}.\${Bar}\`));
75
} else {
76
- throw _ReactErrorProd(18, Foo, Bar);
76
+ throw _ReactErrorProd(Error(18), Foo, Bar);
77
}
78
}
79
})();"
@@ -87,9 +87,9 @@ import invariant from 'shared/invariant';
87
(function () {
88
if (!condition) {
89
if (__DEV__) {
90
- throw _ReactError(\`Expected \${foo} target to be an array; got \${bar}\`);
90
+ throw _ReactError(Error(\`Expected \${foo} target to be an array; got \${bar}\`));
91
} else {
92
- throw _ReactErrorProd(7, foo, bar);
92
+ throw _ReactErrorProd(Error(7), foo, bar);
93
}
94
}
95
})();"
@@ -101,7 +101,7 @@ exports[`error transform should support noMinify option 1`] = `
101
import invariant from 'shared/invariant';
102
(function () {
103
if (!condition) {
104
- throw _ReactError(\`Do not override existing functions.\`);
104
+ throw _ReactError(Error(\`Do not override existing functions.\`));
105
}
106
})();"
107
`;
scripts/error-codes/transform-error-messages.js
+14
-10
@@ -29,9 +29,9 @@ module.exports = function(babel) {
29
//
30
// if (!condition) {
31
// if (__DEV__) {
32
- // throw ReactError(`A ${adj} message that contains ${noun}`);
32
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
33
// } else {
34
- // throw ReactErrorProd(ERR_CODE, adj, noun);
34
+ // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
35
// }
36
// }
37
//
@@ -53,10 +53,12 @@ module.exports = function(babel) {
53
);
54
55
// Outputs:
56
- // throw ReactError(`A ${adj} message that contains ${noun}`);
56
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
57
const devThrow = t.throwStatement(
58
t.callExpression(reactErrorIdentfier, [
59
- t.templateLiteral(errorMsgQuasis, errorMsgExpressions),
59
+ t.callExpression(t.identifier('Error'), [
60
+ t.templateLiteral(errorMsgQuasis, errorMsgExpressions),
61
+ ]),
62
])
63
);
64
@@ -65,7 +67,7 @@ module.exports = function(babel) {
67
//
68
// Outputs:
69
// if (!condition) {
68
- // throw ReactError(`A ${adj} message that contains ${noun}`);
70
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
71
// }
72
path.replaceWith(
73
t.ifStatement(
@@ -92,7 +94,7 @@ module.exports = function(babel) {
94
// Outputs:
95
// /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
96
// if (!condition) {
95
- // throw ReactError(`A ${adj} message that contains ${noun}`);
97
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
98
// }
99
path.replaceWith(
100
t.ifStatement(
@@ -116,10 +118,12 @@ module.exports = function(babel) {
118
);
119
120
// Outputs:
119
- // throw ReactErrorProd(ERR_CODE, adj, noun);
121
+ // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
122
const prodThrow = t.throwStatement(
123
t.callExpression(reactErrorProdIdentfier, [
122
- t.numericLiteral(prodErrorId),
124
+ t.callExpression(t.identifier('Error'), [
125
+ t.numericLiteral(prodErrorId),
126
+ ]),
127
...errorMsgExpressions,
128
])
129
);
@@ -127,9 +131,9 @@ module.exports = function(babel) {
131
// Outputs:
132
// if (!condition) {
133
// if (__DEV__) {
130
- // throw ReactError(`A ${adj} message that contains ${noun}`);
134
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
135
// } else {
132
- // throw ReactErrorProd(ERR_CODE, adj, noun);
136
+ // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
137
// }
138
// }
139
path.replaceWith(
scripts/jest/setupTests.js
+41
-2
@@ -130,6 +130,10 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
130
if (process.env.NODE_ENV === 'production') {
131
// In production, we strip error messages and turn them into codes.
132
// This decodes them back so that the test assertions on them work.
133
+ // 1. `ErrorProxy` decodes error messages at Error construction time and
134
+ // also proxies error instances with `proxyErrorInstance`.
135
+ // 2. `proxyErrorInstance` decodes error messages when the `message`
136
+ // property is changed.
137
const decodeErrorMessage = function(message) {
138
if (!message) {
139
return message;
@@ -150,16 +154,51 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
154
return format.replace(/%s/g, () => args[argIndex++]);
155
};
156
const OriginalError = global.Error;
157
+ // V8's Error.captureStackTrace (used in Jest) fails if the error object is
158
+ // a Proxy, so we need to pass it the unproxied instance.
159
+ const originalErrorInstances = new WeakMap();
160
+ const captureStackTrace = function(error, ...args) {
161
+ return OriginalError.captureStackTrace.call(
162
+ this,
163
+ originalErrorInstances.get(error) ||
164
+ // Sometimes this wrapper receives an already-unproxied instance.
165
+ error,
166
+ ...args
167
+ );
168
+ };
169
+ const proxyErrorInstance = error => {
170
+ const proxy = new Proxy(error, {
171
+ set(target, key, value, receiver) {
172
+ if (key === 'message') {
173
+ return Reflect.set(
174
+ target,
175
+ key,
176
+ decodeErrorMessage(value),
177
+ receiver
178
+ );
179
+ }
180
+ return Reflect.set(target, key, value, receiver);
181
+ },
182
+ });
183
+ originalErrorInstances.set(proxy, error);
184
+ return proxy;
185
+ };
186
const ErrorProxy = new Proxy(OriginalError, {
187
apply(target, thisArg, argumentsList) {
188
const error = Reflect.apply(target, thisArg, argumentsList);
189
error.message = decodeErrorMessage(error.message);
157
- return error;
190
+ return proxyErrorInstance(error);
191
},
192
construct(target, argumentsList, newTarget) {
193
const error = Reflect.construct(target, argumentsList, newTarget);
194
error.message = decodeErrorMessage(error.message);
162
- return error;
195
+ return proxyErrorInstance(error);
196
+ },
197
+ get(target, key, receiver) {
198
+ if (key === 'captureStackTrace') {
199
+ return captureStackTrace;
200
+ }
201
+ return Reflect.get(target, key, receiver);
202
},
203
});
204
ErrorProxy.OriginalError = OriginalError;