diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js
index ffa0083ae740..d2f1062ce83a 100644
--- a/packages/react/src/__tests__/index-test.js
+++ b/packages/react/src/__tests__/index-test.js
@@ -42,6 +42,8 @@ describe('Carbon Components React', () => {
"DatePickerSkeleton",
"Dropdown",
"DropdownSkeleton",
+ "ErrorBoundary",
+ "ErrorBoundaryContext",
"ExpandableTile",
"FileUploader",
"FileUploaderButton",
diff --git a/packages/react/src/components/ErrorBoundary/ErrorBoundary-story.js b/packages/react/src/components/ErrorBoundary/ErrorBoundary-story.js
new file mode 100644
index 000000000000..01361833d2d9
--- /dev/null
+++ b/packages/react/src/components/ErrorBoundary/ErrorBoundary-story.js
@@ -0,0 +1,82 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, { useState } from 'react';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+import { ErrorBoundary, ErrorBoundaryContext } from './';
+import Button from '../Button';
+
+storiesOf('ErrorBoundary', module)
+ .add('default', () => {
+ function DemoComponent() {
+ const [shouldThrowError, setShouldThrowError] = useState(false);
+
+ function onClick() {
+ setShouldThrowError(!shouldThrowError);
+ }
+
+ return (
+ <>
+
+
+ }>
+
+
+
+ >
+ );
+ }
+
+ function Fallback() {
+ return 'Whoops';
+ }
+
+ function ThrowError({ shouldThrowError }) {
+ if (shouldThrowError) {
+ throw new Error('Component threw error');
+ }
+
+ return 'Successfully rendered';
+ }
+
+ return ;
+ })
+ .add('with custom context', () => {
+ function DemoComponent() {
+ const [shouldThrowError, setShouldThrowError] = useState(false);
+
+ function onClick() {
+ setShouldThrowError(!shouldThrowError);
+ }
+
+ return (
+
+
+
+ }>
+
+
+
+
+ );
+ }
+
+ function Fallback() {
+ return 'Whoops';
+ }
+
+ function ThrowError({ shouldThrowError }) {
+ if (shouldThrowError) {
+ throw new Error('Component threw error');
+ }
+
+ return 'Successfully rendered';
+ }
+
+ return ;
+ });
diff --git a/packages/react/src/components/ErrorBoundary/ErrorBoundary.js b/packages/react/src/components/ErrorBoundary/ErrorBoundary.js
new file mode 100644
index 000000000000..339402a05470
--- /dev/null
+++ b/packages/react/src/components/ErrorBoundary/ErrorBoundary.js
@@ -0,0 +1,63 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { ErrorBoundaryContext } from './ErrorBoundaryContext';
+
+/**
+ * React introduced additional lifecycle methods in v16 for capturing errors
+ * that occur in a specific sub-tree of components. This component helps to
+ * consolidate some of the duplication that occurs when using these lifecycle
+ * methods across a codebase. In addition, it allows you to specify the fallback
+ * UI to display when an error occurs in the sub-tree through the `fallback`
+ * prop.
+ *
+ * This component roughly follows the React.js docs example code for these
+ * methods. In addition, it takes advantage of an `ErrorBoundaryContext` so that
+ * consumers can specify their own logic for logging errors. For example,
+ * reporting an error in the UI to an external service for every `ErrorBoundary`
+ * used.
+ *
+ * Reference:
+ * https://reactjs.org/docs/error-boundaries.html#introducing-error-boundaries
+ */
+export default class ErrorBoundary extends React.Component {
+ static propTypes = {
+ children: PropTypes.node,
+ fallback: PropTypes.node,
+ };
+
+ static contextType = ErrorBoundaryContext;
+
+ static getDerivedStateFromError() {
+ return {
+ hasError: true,
+ };
+ }
+
+ state = {
+ hasError: false,
+ };
+
+ componentDidCatch(error, info) {
+ this.context.log(error, info);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.children !== this.props.children) {
+ this.setState({ hasError: false });
+ }
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
+ }
+ return this.props.children;
+ }
+}
diff --git a/packages/react/src/components/ErrorBoundary/ErrorBoundaryContext.js b/packages/react/src/components/ErrorBoundary/ErrorBoundaryContext.js
new file mode 100644
index 000000000000..04977f157cac
--- /dev/null
+++ b/packages/react/src/components/ErrorBoundary/ErrorBoundaryContext.js
@@ -0,0 +1,14 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { createContext } from 'react';
+
+export const ErrorBoundaryContext = createContext({
+ log(error, info) {
+ console.log(info.componentStack);
+ },
+});
diff --git a/packages/react/src/components/ErrorBoundary/__tests__/ErrorBoundary-test.js b/packages/react/src/components/ErrorBoundary/__tests__/ErrorBoundary-test.js
new file mode 100644
index 000000000000..cddd95a3247f
--- /dev/null
+++ b/packages/react/src/components/ErrorBoundary/__tests__/ErrorBoundary-test.js
@@ -0,0 +1,123 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { render, cleanup } from '@carbon/test-utils/react';
+import React, { useState } from 'react';
+import { ErrorBoundary, ErrorBoundaryContext } from '../';
+
+describe('ErrorBoundary', () => {
+ afterEach(cleanup);
+
+ it('should render children and not a fallback if no error is caught', () => {
+ function MockComponent() {
+ return mock;
+ }
+
+ function MockFallback() {
+ return mock;
+ }
+
+ const { container } = render(
+ }>
+
+
+ );
+
+ const component = container.querySelector('[data-test-id="mock"]');
+ const fallback = container.querySelector('[data-test-id="fallback"]');
+
+ expect(component).toBeDefined();
+ expect(fallback).toBe(null);
+ });
+
+ it('should render a fallback if an error is caught', () => {
+ console.error = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ function MockComponent() {
+ throw new Error('test error');
+ }
+
+ function MockFallback() {
+ return mock;
+ }
+
+ const log = jest.fn();
+
+ const { container } = render(
+
+ }>
+
+
+
+ );
+
+ const component = container.querySelector('[data-test-id="mock"]');
+ const fallback = container.querySelector('[data-test-id="fallback"]');
+
+ expect(component).toBe(null);
+ expect(fallback).toBeDefined();
+ expect(console.error).toHaveBeenCalled();
+ expect(log).toHaveBeenCalled();
+
+ console.error.mockRestore();
+ });
+
+ it('should reset from fallback if children have changed', () => {
+ console.error = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ let content = null;
+ let fallback = null;
+ let button = null;
+
+ function ThrowError({ shouldThrowError }) {
+ if (shouldThrowError) {
+ throw new Error('test error');
+ }
+ return (content = element)}>mock;
+ }
+
+ function MockFallback() {
+ return (fallback = element)}>mock;
+ }
+
+ function MockTest() {
+ const [shouldThrow, setShouldThrow] = useState(false);
+
+ function onClick() {
+ setShouldThrow(!shouldThrow);
+ }
+
+ return (
+
+
+ }>
+
+
+
+ );
+ }
+
+ render();
+
+ expect(content).toBeDefined();
+ expect(fallback).toBe(null);
+
+ button.click();
+
+ expect(content).toBe(null);
+ expect(fallback).toBeDefined();
+
+ button.click();
+
+ expect(content).toBeDefined();
+ expect(fallback).toBe(null);
+
+ console.error.mockRestore();
+ });
+});
diff --git a/packages/react/src/components/ErrorBoundary/index.js b/packages/react/src/components/ErrorBoundary/index.js
new file mode 100644
index 000000000000..cbba37bd349e
--- /dev/null
+++ b/packages/react/src/components/ErrorBoundary/index.js
@@ -0,0 +1,9 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+export { default as ErrorBoundary } from './ErrorBoundary';
+export { ErrorBoundaryContext } from './ErrorBoundaryContext';
diff --git a/packages/react/src/index.js b/packages/react/src/index.js
index cc2d274d0d74..c040649f2d61 100644
--- a/packages/react/src/index.js
+++ b/packages/react/src/index.js
@@ -47,6 +47,10 @@ export {
export DatePicker from './components/DatePicker';
export DatePickerInput from './components/DatePickerInput';
export Dropdown from './components/Dropdown';
+export {
+ ErrorBoundary,
+ ErrorBoundaryContext,
+} from './components/ErrorBoundary';
export FileUploader, {
Filename,
FileUploaderButton,