diff --git a/packages/shared/ReactError.js b/packages/shared/ReactError.js
index 2c493f7d81cd..0a5b1ed222d8 100644
--- a/packages/shared/ReactError.js
+++ b/packages/shared/ReactError.js
@@ -6,12 +6,11 @@
*
*/
-// Do not require this module directly! Use a normal error constructor with
+// Do not require this module directly! Use normal `invariant` calls with
// template literal strings. The messages will be converted to ReactError during
// build, and in production they will be minified.
-function ReactError(message) {
- const error = new Error(message);
+function ReactError(error) {
error.name = 'Invariant Violation';
return error;
}
diff --git a/packages/shared/ReactErrorProd.js b/packages/shared/ReactErrorProd.js
index 3b3996e1366d..b909e2180809 100644
--- a/packages/shared/ReactErrorProd.js
+++ b/packages/shared/ReactErrorProd.js
@@ -6,20 +6,21 @@
*
*/
-// Do not require this module directly! Use a normal error constructor with
+// Do not require this module directly! Use normal `invariant` calls with
// template literal strings. The messages will be converted to ReactError during
// build, and in production they will be minified.
-function ReactErrorProd(code) {
+function ReactErrorProd(error) {
+ const code = error.message;
let url = 'https://reactjs.org/docs/error-decoder.html?invariant=' + code;
for (let i = 1; i < arguments.length; i++) {
url += '&args[]=' + encodeURIComponent(arguments[i]);
}
- return new Error(
+ error.message =
`Minified React error #${code}; visit ${url} for the full message or ` +
- 'use the non-minified dev environment for full errors and additional ' +
- 'helpful warnings. ',
- );
+ 'use the non-minified dev environment for full errors and additional ' +
+ 'helpful warnings. ';
+ return error;
}
export default ReactErrorProd;
diff --git a/packages/shared/__tests__/ReactErrorProd-test.internal.js b/packages/shared/__tests__/ReactErrorProd-test.internal.js
index b8984d13d52a..62d6da32c5c2 100644
--- a/packages/shared/__tests__/ReactErrorProd-test.internal.js
+++ b/packages/shared/__tests__/ReactErrorProd-test.internal.js
@@ -36,7 +36,7 @@ describe('ReactErrorProd', () => {
it('should throw with the correct number of `%s`s in the URL', () => {
expect(function() {
- throw ReactErrorProd(124, 'foo', 'bar');
+ throw ReactErrorProd(Error(124), 'foo', 'bar');
}).toThrowError(
'Minified React error #124; visit ' +
'https://reactjs.org/docs/error-decoder.html?invariant=124&args[]=foo&args[]=bar' +
@@ -45,7 +45,7 @@ describe('ReactErrorProd', () => {
);
expect(function() {
- throw ReactErrorProd(20);
+ throw ReactErrorProd(Error(20));
}).toThrowError(
'Minified React error #20; visit ' +
'https://reactjs.org/docs/error-decoder.html?invariant=20' +
@@ -54,7 +54,7 @@ describe('ReactErrorProd', () => {
);
expect(function() {
- throw ReactErrorProd(77, '
', '&?bar');
+ throw ReactErrorProd(Error(77), '
', '&?bar');
}).toThrowError(
'Minified React error #77; visit ' +
'https://reactjs.org/docs/error-decoder.html?invariant=77&args[]=%3Cdiv%3E&args[]=%26%3Fbar' +
diff --git a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap
index 3887fcc15bc8..7819a3147469 100644
--- a/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap
+++ b/scripts/error-codes/__tests__/__snapshots__/transform-error-messages.js.snap
@@ -6,7 +6,7 @@ exports[`error transform should correctly transform invariants that are not in t
import invariant from 'shared/invariant';
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/(function () {
if (!condition) {
- throw _ReactError(\`This is not a real error message.\`);
+ throw _ReactError(Error(\`This is not a real error message.\`));
}
})();"
`;
@@ -17,7 +17,7 @@ exports[`error transform should handle escaped characters 1`] = `
import invariant from 'shared/invariant';
/*FIXME (minify-errors-in-prod): Unminified error message in production build!*/(function () {
if (!condition) {
- throw _ReactError(\`What's up?\`);
+ throw _ReactError(Error(\`What's up?\`));
}
})();"
`;
@@ -30,18 +30,18 @@ import invariant from 'shared/invariant';
(function () {
if (!condition) {
if (__DEV__) {
- throw _ReactError(\`Do not override existing functions.\`);
+ throw _ReactError(Error(\`Do not override existing functions.\`));
} else {
- throw _ReactErrorProd(16);
+ throw _ReactErrorProd(Error(16));
}
}
})();
(function () {
if (!condition) {
if (__DEV__) {
- throw _ReactError(\`Do not override existing functions.\`);
+ throw _ReactError(Error(\`Do not override existing functions.\`));
} else {
- throw _ReactErrorProd(16);
+ throw _ReactErrorProd(Error(16));
}
}
})();"
@@ -55,9 +55,9 @@ import invariant from 'shared/invariant';
(function () {
if (!condition) {
if (__DEV__) {
- throw _ReactError(\`Do not override existing functions.\`);
+ throw _ReactError(Error(\`Do not override existing functions.\`));
} else {
- throw _ReactErrorProd(16);
+ throw _ReactErrorProd(Error(16));
}
}
})();"
@@ -71,9 +71,9 @@ import invariant from 'shared/invariant';
(function () {
if (!condition) {
if (__DEV__) {
- throw _ReactError(\`Expected a component class, got \${Foo}.\${Bar}\`);
+ throw _ReactError(Error(\`Expected a component class, got \${Foo}.\${Bar}\`));
} else {
- throw _ReactErrorProd(18, Foo, Bar);
+ throw _ReactErrorProd(Error(18), Foo, Bar);
}
}
})();"
@@ -87,9 +87,9 @@ import invariant from 'shared/invariant';
(function () {
if (!condition) {
if (__DEV__) {
- throw _ReactError(\`Expected \${foo} target to be an array; got \${bar}\`);
+ throw _ReactError(Error(\`Expected \${foo} target to be an array; got \${bar}\`));
} else {
- throw _ReactErrorProd(7, foo, bar);
+ throw _ReactErrorProd(Error(7), foo, bar);
}
}
})();"
@@ -101,7 +101,7 @@ exports[`error transform should support noMinify option 1`] = `
import invariant from 'shared/invariant';
(function () {
if (!condition) {
- throw _ReactError(\`Do not override existing functions.\`);
+ throw _ReactError(Error(\`Do not override existing functions.\`));
}
})();"
`;
diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js
index f0331579270e..f18ba3f6ffd4 100644
--- a/scripts/error-codes/transform-error-messages.js
+++ b/scripts/error-codes/transform-error-messages.js
@@ -29,9 +29,9 @@ module.exports = function(babel) {
//
// if (!condition) {
// if (__DEV__) {
- // throw ReactError(`A ${adj} message that contains ${noun}`);
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
// } else {
- // throw ReactErrorProd(ERR_CODE, adj, noun);
+ // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
// }
// }
//
@@ -53,10 +53,12 @@ module.exports = function(babel) {
);
// Outputs:
- // throw ReactError(`A ${adj} message that contains ${noun}`);
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
const devThrow = t.throwStatement(
t.callExpression(reactErrorIdentfier, [
- t.templateLiteral(errorMsgQuasis, errorMsgExpressions),
+ t.callExpression(t.identifier('Error'), [
+ t.templateLiteral(errorMsgQuasis, errorMsgExpressions),
+ ]),
])
);
@@ -65,7 +67,7 @@ module.exports = function(babel) {
//
// Outputs:
// if (!condition) {
- // throw ReactError(`A ${adj} message that contains ${noun}`);
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
// }
path.replaceWith(
t.ifStatement(
@@ -92,7 +94,7 @@ module.exports = function(babel) {
// Outputs:
// /* FIXME (minify-errors-in-prod): Unminified error message in production build! */
// if (!condition) {
- // throw ReactError(`A ${adj} message that contains ${noun}`);
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
// }
path.replaceWith(
t.ifStatement(
@@ -116,10 +118,12 @@ module.exports = function(babel) {
);
// Outputs:
- // throw ReactErrorProd(ERR_CODE, adj, noun);
+ // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
const prodThrow = t.throwStatement(
t.callExpression(reactErrorProdIdentfier, [
- t.numericLiteral(prodErrorId),
+ t.callExpression(t.identifier('Error'), [
+ t.numericLiteral(prodErrorId),
+ ]),
...errorMsgExpressions,
])
);
@@ -127,9 +131,9 @@ module.exports = function(babel) {
// Outputs:
// if (!condition) {
// if (__DEV__) {
- // throw ReactError(`A ${adj} message that contains ${noun}`);
+ // throw ReactError(Error(`A ${adj} message that contains ${noun}`));
// } else {
- // throw ReactErrorProd(ERR_CODE, adj, noun);
+ // throw ReactErrorProd(Error(ERR_CODE), adj, noun);
// }
// }
path.replaceWith(
diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js
index 92ef339f6728..b2933bc5fba5 100644
--- a/scripts/jest/setupTests.js
+++ b/scripts/jest/setupTests.js
@@ -130,6 +130,10 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
if (process.env.NODE_ENV === 'production') {
// In production, we strip error messages and turn them into codes.
// This decodes them back so that the test assertions on them work.
+ // 1. `ErrorProxy` decodes error messages at Error construction time and
+ // also proxies error instances with `proxyErrorInstance`.
+ // 2. `proxyErrorInstance` decodes error messages when the `message`
+ // property is changed.
const decodeErrorMessage = function(message) {
if (!message) {
return message;
@@ -150,16 +154,51 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
return format.replace(/%s/g, () => args[argIndex++]);
};
const OriginalError = global.Error;
+ // V8's Error.captureStackTrace (used in Jest) fails if the error object is
+ // a Proxy, so we need to pass it the unproxied instance.
+ const originalErrorInstances = new WeakMap();
+ const captureStackTrace = function(error, ...args) {
+ return OriginalError.captureStackTrace.call(
+ this,
+ originalErrorInstances.get(error) ||
+ // Sometimes this wrapper receives an already-unproxied instance.
+ error,
+ ...args
+ );
+ };
+ const proxyErrorInstance = error => {
+ const proxy = new Proxy(error, {
+ set(target, key, value, receiver) {
+ if (key === 'message') {
+ return Reflect.set(
+ target,
+ key,
+ decodeErrorMessage(value),
+ receiver
+ );
+ }
+ return Reflect.set(target, key, value, receiver);
+ },
+ });
+ originalErrorInstances.set(proxy, error);
+ return proxy;
+ };
const ErrorProxy = new Proxy(OriginalError, {
apply(target, thisArg, argumentsList) {
const error = Reflect.apply(target, thisArg, argumentsList);
error.message = decodeErrorMessage(error.message);
- return error;
+ return proxyErrorInstance(error);
},
construct(target, argumentsList, newTarget) {
const error = Reflect.construct(target, argumentsList, newTarget);
error.message = decodeErrorMessage(error.message);
- return error;
+ return proxyErrorInstance(error);
+ },
+ get(target, key, receiver) {
+ if (key === 'captureStackTrace') {
+ return captureStackTrace;
+ }
+ return Reflect.get(target, key, receiver);
},
});
ErrorProxy.OriginalError = OriginalError;