diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index a008fa96378c2..ba696073f82a5 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -45,6 +45,7 @@ import {
cloneElementWithValidation,
} from './ReactElementValidator';
import ReactSharedInternals from './ReactSharedInternals';
+import {error, warn} from './withComponentStack';
import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
const React = {
@@ -65,6 +66,9 @@ const React = {
lazy,
memo,
+ error,
+ warn,
+
useCallback,
useContext,
useEffect,
diff --git a/packages/react/src/__tests__/withComponentStack-test.js b/packages/react/src/__tests__/withComponentStack-test.js
new file mode 100644
index 0000000000000..e46d7f2e54997
--- /dev/null
+++ b/packages/react/src/__tests__/withComponentStack-test.js
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+function normalizeCodeLocInfo(str) {
+ return str && str.replace(/at .+?:\d+/g, 'at **');
+}
+
+function expectHelper(spy, prefix, ...expectedArgs) {
+ const expectedStack = expectedArgs.pop();
+
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ const actualArgs = spy.calls.mostRecent().args;
+
+ let actualStack = undefined;
+ if (expectedStack !== undefined) {
+ actualStack = actualArgs.pop();
+ expect(normalizeCodeLocInfo(actualStack)).toBe(expectedStack);
+ }
+
+ expect(actualArgs).toHaveLength(expectedArgs.length);
+ actualArgs.forEach((actualArg, index) => {
+ const expectedArg = expectedArgs[index];
+ expect(actualArg).toBe(
+ index === 0 ? `${prefix}: ${expectedArg}` : expectedArg,
+ );
+ });
+}
+
+function expectMessageAndStack(...expectedArgs) {
+ expectHelper(console.error, 'error', ...expectedArgs);
+ expectHelper(console.warn, 'warn', ...expectedArgs);
+}
+
+describe('withComponentStack', () => {
+ let React = null;
+ let ReactTestRenderer = null;
+ let error = null;
+ let scheduler = null;
+ let warn = null;
+
+ beforeEach(() => {
+ jest.resetModules();
+ jest.mock('scheduler', () => require('scheduler/unstable_mock'));
+
+ React = require('react');
+ ReactTestRenderer = require('react-test-renderer');
+ scheduler = require('scheduler');
+
+ error = React.error;
+ warn = React.warn;
+
+ spyOnDevAndProd(console, 'error');
+ spyOnDevAndProd(console, 'warn');
+ });
+
+ if (!__DEV__) {
+ it('does nothing in production mode', () => {
+ error('error');
+ warn('warning');
+
+ expect(console.error).toHaveBeenCalledTimes(0);
+ expect(console.warn).toHaveBeenCalledTimes(0);
+ });
+ }
+
+ if (__DEV__) {
+ it('does not include component stack when called outside of render', () => {
+ error('error: logged outside of render');
+ warn('warn: logged outside of render');
+ expectMessageAndStack('logged outside of render', undefined);
+ });
+
+ it('should support multiple args', () => {
+ function Component() {
+ error('error: number:', 123, 'boolean:', true);
+ warn('warn: number:', 123, 'boolean:', true);
+ return null;
+ }
+
+ ReactTestRenderer.create();
+
+ expectMessageAndStack(
+ 'number:',
+ 123,
+ 'boolean:',
+ true,
+ '\n in Component (at **)',
+ );
+ });
+
+ it('includes component stack when called from a render method', () => {
+ class Parent extends React.Component {
+ render() {
+ return ;
+ }
+ }
+
+ function Child() {
+ error('error: logged in child render method');
+ warn('warn: logged in child render method');
+ return null;
+ }
+
+ ReactTestRenderer.create();
+
+ expectMessageAndStack(
+ 'logged in child render method',
+ '\n in Child (at **)' + '\n in Parent (at **)',
+ );
+ });
+
+ it('includes component stack when called from a render phase lifecycle method', () => {
+ function Parent() {
+ return ;
+ }
+
+ class Child extends React.Component {
+ UNSAFE_componentWillMount() {
+ error('error: logged in child cWM lifecycle');
+ warn('warn: logged in child cWM lifecycle');
+ }
+ render() {
+ return null;
+ }
+ }
+
+ ReactTestRenderer.create();
+
+ expectMessageAndStack(
+ 'logged in child cWM lifecycle',
+ '\n in Child (at **)' + '\n in Parent (at **)',
+ );
+ });
+
+ it('includes component stack when called from a commit phase lifecycle method', () => {
+ function Parent() {
+ return ;
+ }
+
+ class Child extends React.Component {
+ componentDidMount() {
+ error('error: logged in child cDM lifecycle');
+ warn('warn: logged in child cDM lifecycle');
+ }
+ render() {
+ return null;
+ }
+ }
+
+ ReactTestRenderer.create();
+
+ expectMessageAndStack(
+ 'logged in child cDM lifecycle',
+ '\n in Child (at **)' + '\n in Parent (at **)',
+ );
+ });
+
+ it('includes component stack when called from a passive effect handler', () => {
+ class Parent extends React.Component {
+ render() {
+ return ;
+ }
+ }
+
+ function Child() {
+ React.useEffect(() => {
+ error('error: logged in child render method');
+ warn('warn: logged in child render method');
+ });
+ return null;
+ }
+
+ ReactTestRenderer.create();
+
+ scheduler.flushAll(); // Flush passive effects
+
+ expectMessageAndStack(
+ 'logged in child render method',
+ '\n in Child (at **)' + '\n in Parent (at **)',
+ );
+ });
+ }
+});
diff --git a/packages/react/src/withComponentStack.js b/packages/react/src/withComponentStack.js
new file mode 100644
index 0000000000000..62aa7ccde2009
--- /dev/null
+++ b/packages/react/src/withComponentStack.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import ReactSharedInternals from 'shared/ReactSharedInternals';
+
+function noop() {}
+
+let error = noop;
+let warn = noop;
+if (__DEV__) {
+ const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
+
+ error = function() {
+ const stack = ReactDebugCurrentFrame.getStackAddendum();
+ if (stack !== '') {
+ const length = arguments.length;
+ const args = new Array(length + 1);
+ for (let i = 0; i < length; i++) {
+ args[i] = arguments[i];
+ }
+ args[length] = stack;
+ console.error.apply(console, args);
+ } else {
+ console.error.apply(console, arguments);
+ }
+ };
+
+ warn = function() {
+ const stack = ReactDebugCurrentFrame.getStackAddendum();
+ if (stack !== '') {
+ const length = arguments.length;
+ const args = new Array(length + 1);
+ for (let i = 0; i < length; i++) {
+ args[i] = arguments[i];
+ }
+ args[length] = stack;
+ console.warn.apply(console, args);
+ } else {
+ console.warn.apply(console, arguments);
+ }
+ };
+}
+
+export {error, warn};