diff --git a/src/core/__tests__/ReactErrorBoundaries-test.js b/src/core/__tests__/ReactErrorBoundaries-test.js
new file mode 100644
index 00000000000000..4cea533e3e9393
--- /dev/null
+++ b/src/core/__tests__/ReactErrorBoundaries-test.js
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2013-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+var React;
+var ReactDOM;
+
+describe('ReactErrorBoundaries', function() {
+
+ beforeEach(function() {
+ ReactDOM = require('ReactDOM');
+ React = require('React');
+ });
+
+ it('catches errors from children', function() {
+ var log = [];
+
+ class Box extends React.Component {
+ constructor(props) {
+ super(props);
+ this._errorMessage = undefined;
+ }
+ render() {
+ if (this._errorMessage) {
+ log.push('Box renderError');
+ return
Error: {this._errorMessage}
;
+ }
+ log.push('Box render');
+ var ref = function(x) {
+ log.push('Inquisitive ref ' + x);
+ };
+ return (
+
+ );
+ }
+ handleError(e) {
+ this._errorMessage = e.message;
+ }
+ componentDidMount() {
+ log.push('Box componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('Box componentWillUnmount');
+ }
+ }
+
+ class Inquisitive extends React.Component {
+ render() {
+ log.push('Inquisitive render');
+ return What is love?
;
+ }
+ componentDidMount() {
+ log.push('Inquisitive componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('Inquisitive componentWillUnmount');
+ }
+ }
+
+ class Angry extends React.Component {
+ render() {
+ log.push('Angry render');
+ throw new Error('Please, do not render me.');
+ }
+ componentDidMount() {
+ log.push('Angry componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('Angry componentWillUnmount');
+ }
+ }
+
+ var container = document.createElement('div');
+ ReactDOM.render(, container);
+ expect(container.textContent).toBe('Error: Please, do not render me.');
+ expect(log).toEqual([
+ 'Box render',
+ 'Inquisitive render',
+ 'Angry render',
+ 'Inquisitive ref null',
+ 'Inquisitive componentWillUnmount',
+ 'Angry componentWillUnmount',
+ 'Box renderError',
+ ]);
+ });
+});
diff --git a/src/renderers/dom/client/ReactReconcileTransaction.js b/src/renderers/dom/client/ReactReconcileTransaction.js
index cc71061bb2bd66..17776aa99c1dc6 100644
--- a/src/renderers/dom/client/ReactReconcileTransaction.js
+++ b/src/renderers/dom/client/ReactReconcileTransaction.js
@@ -137,6 +137,19 @@ var Mixin = {
return this.reactMountReady;
},
+ /**
+ * Save current transaction state -- if the return value from this method is
+ * passed to `rollback`, the transaction will be reset to that state.
+ */
+ checkpoint: function() {
+ // reactMountReady is the our only stateful wrapper
+ return this.reactMountReady.checkpoint();
+ },
+
+ rollback: function(checkpoint) {
+ this.reactMountReady.rollback(checkpoint);
+ },
+
/**
* `PooledClass` looks for this, and will invoke this before allowing this
* instance to be reused.
diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js
index b341eec2f8d37f..585ef6a7876c28 100644
--- a/src/renderers/shared/reconciler/ReactCompositeComponent.js
+++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js
@@ -28,6 +28,12 @@ var invariant = require('invariant');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
var warning = require('warning');
+/**
+ * Used to indicate that no error has been thrown (since you can actually throw
+ * null, undefined, and seemingly anything else).
+ */
+var NO_ERROR = {};
+
function getDeclarationErrorAddendum(component) {
var owner = component._currentElement._owner || null;
if (owner) {
@@ -99,6 +105,7 @@ var ReactCompositeComponentMixin = {
this._instance = null;
this._nativeParent = null;
this._nativeContainerInfo = null;
+ this._renderedNodeType = null;
// See ReactUpdateQueue
this._pendingElement = null;
@@ -106,9 +113,8 @@ var ReactCompositeComponentMixin = {
this._pendingReplaceState = false;
this._pendingForceUpdate = false;
- this._renderedNodeType = null;
+ this._caughtError = NO_ERROR;
this._renderedComponent = null;
-
this._context = null;
this._mountOrder = 0;
this._topLevelWrapper = null;
@@ -278,6 +284,36 @@ var ReactCompositeComponentMixin = {
this._pendingReplaceState = false;
this._pendingForceUpdate = false;
+ var markup;
+ if (inst.handleError) {
+ var checkpoint = transaction.checkpoint();
+ try {
+ markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
+ } catch (e) {
+ this._caughtError = e;
+ inst.handleError(e);
+ this._renderedComponent.unmountComponent();
+ transaction.rollback(checkpoint);
+
+ // Try again - we've informed the component about the error, so they can render an error message this time.
+ // If this throws again, the error will bubble up (and can be caught by a higher error boundary).
+ markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
+ // Don't call componentDidMount (TODO: wait, what? why not?)
+ return markup;
+ }
+ } else {
+ markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
+ }
+
+ if (inst.componentDidMount) {
+ transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
+ }
+
+ return markup;
+ },
+
+ performInitialMount: function(renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
+ var inst = this._instance;
if (inst.componentWillMount) {
inst.componentWillMount();
// When mounting, calls to `setState` by `componentWillMount` will set
@@ -304,9 +340,6 @@ var ReactCompositeComponentMixin = {
nativeContainerInfo,
this._processChildContext(context)
);
- if (inst.componentDidMount) {
- transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
- }
return markup;
},
@@ -328,10 +361,12 @@ var ReactCompositeComponentMixin = {
inst.componentWillUnmount();
}
- ReactReconciler.unmountComponent(this._renderedComponent);
- this._renderedNodeType = null;
- this._renderedComponent = null;
- this._instance = null;
+ if (this._renderedComponent) {
+ ReactReconciler.unmountComponent(this._renderedComponent);
+ this._renderedNodeType = null;
+ this._renderedComponent = null;
+ this._instance = null;
+ }
// Reset pending fields
// Even if this component is scheduled for another update in ReactUpdates,
@@ -786,7 +821,8 @@ var ReactCompositeComponentMixin = {
*/
_renderValidatedComponentWithoutOwnerOrContext: function() {
var inst = this._instance;
- var renderedComponent = inst.render();
+ var renderedComponent;
+ renderedComponent = inst.render();
if (__DEV__) {
// We allow auto-mocks to proceed as if they're returning null.
if (typeof renderedComponent === 'undefined' &&
diff --git a/src/shared/utils/CallbackQueue.js b/src/shared/utils/CallbackQueue.js
index 089d6802920c00..2fca444c49b2f3 100644
--- a/src/shared/utils/CallbackQueue.js
+++ b/src/shared/utils/CallbackQueue.js
@@ -72,6 +72,17 @@ assign(CallbackQueue.prototype, {
}
},
+ checkpoint: function() {
+ return this._callbacks ? this._callbacks.length : 0;
+ },
+
+ rollback: function(len) {
+ if (this._callbacks) {
+ this._callbacks.length = len;
+ this._contexts.length = len;
+ }
+ },
+
/**
* Resets the internal queue.
*