diff --git a/src/index.js b/src/index.js
index 4deebcb9..9c263937 100644
--- a/src/index.js
+++ b/src/index.js
@@ -59,7 +59,7 @@ function renderToString(vnode, context, opts, inner, isSvgMode) {
nodeName = getComponentName(nodeName);
}
else {
- let rendered;
+ let c, rendered;
if (!nodeName.prototype || typeof nodeName.prototype.render!=='function') {
// stateless functional components
@@ -67,7 +67,7 @@ function renderToString(vnode, context, opts, inner, isSvgMode) {
}
else {
// class-based components
- let c = new nodeName(props, context);
+ c = new nodeName(props, context);
// turn off stateful re-rendering:
c._dirty = c.__d = true;
c.props = props;
@@ -81,7 +81,17 @@ function renderToString(vnode, context, opts, inner, isSvgMode) {
}
}
- return renderToString(rendered, context, opts, opts.shallowHighOrder!==false);
+ try {
+ return renderToString(rendered, context, opts, opts.shallowHighOrder!==false);
+ }
+ catch (error) {
+ if (c && c.componentDidCatch) {
+ c.componentDidCatch(error);
+ rendered = c.render(c.props, c.state, c.context);
+ return renderToString(rendered, context, opts, opts.shallowHighOrder!==false);
+ }
+ throw error;
+ }
}
}
diff --git a/test/render.js b/test/render.js
index 37c145ac..1c69a4fe 100644
--- a/test/render.js
+++ b/test/render.js
@@ -606,4 +606,258 @@ describe('render', () => {
expect(Bar).to.have.been.calledOnce.and.calledWithMatch({ count: 1 });
});
});
+
+ describe('Error Handling', () => {
+ it('should invoke ErrorBoundary\'s componentDidCatch from an error thrown in ComponentThatThrows\' getDerivedStateFromProps', () => {
+ const ERROR_MESSAGE = 'getDerivedStateFromProps error',
+ THE_ERROR = new Error(ERROR_MESSAGE);
+ class ComponentThatThrows extends Component {
+ static getDerivedStateFromProps() {
+ throw THE_ERROR;
+ }
+ componentDidCatch(error) {}
+ render(props) {
+ return
;
+ }
+ }
+ class ComponentThatRenders extends Component {
+ componentDidCatch(error) {}
+ render(props) {
+ return ;
+ }
+ }
+ class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { throwError: true };
+ }
+ componentDidCatch(error) {
+ this.setState({ throwError: false });
+ }
+ render(props, { throwError }) {
+ return throwError
+ ?
+ : ;
+ }
+ }
+ class App extends Component {
+ componentDidCatch(error) {}
+ render(props) {
+ return ;
+ }
+ }
+
+ spy(ComponentThatThrows.prototype.constructor, 'getDerivedStateFromProps');
+ spy(ComponentThatThrows.prototype, 'componentDidCatch');
+ spy(ComponentThatThrows.prototype, 'render');
+ spy(ComponentThatRenders.prototype, 'componentDidCatch');
+ spy(ComponentThatRenders.prototype, 'render');
+ spy(ErrorBoundary.prototype, 'componentDidCatch');
+ spy(ErrorBoundary.prototype, 'render');
+ spy(App.prototype, 'componentDidCatch');
+ spy(App.prototype, 'render');
+
+ render();
+
+ // ComponentThatThrows
+ expect(ComponentThatThrows.prototype.constructor.getDerivedStateFromProps)
+ .to.have.been.calledOnce
+ .and.to.throw(Error, ERROR_MESSAGE);
+
+ expect(ComponentThatThrows.prototype.componentDidCatch)
+ .to.not.have.been.called;
+
+ expect(ComponentThatThrows.prototype.render)
+ .to.not.have.been.called;
+
+ // ComponentThatRenders
+ expect(ComponentThatRenders.prototype.componentDidCatch)
+ .to.not.have.been.called;
+
+ expect(ComponentThatRenders.prototype.render)
+ .to.have.been.calledOnce
+ .and.to.not.throw();
+
+ // ErrorBoundary
+ expect(ErrorBoundary.prototype.componentDidCatch)
+ .to.have.been.calledOnce
+ .and.calledWithExactly(THE_ERROR);
+
+ expect(ErrorBoundary.prototype.render)
+ .to.have.been.calledTwice;
+
+ // App
+ expect(App.prototype.render)
+ .to.have.been.calledOnce;
+
+ expect(App.prototype.componentDidCatch)
+ .to.not.have.been.called;
+ });
+
+ it('should invoke ErrorBoundary\'s componentDidCatch from an error thrown in ComponentThatThrows\' componentWillMount', () => {
+ const ERROR_MESSAGE = 'componentWillMount error',
+ THE_ERROR = new Error(ERROR_MESSAGE);
+ class ComponentThatThrows extends Component {
+ componentWillMount() {
+ throw THE_ERROR;
+ }
+ componentDidCatch(error) {}
+ render(props) {
+ return ;
+ }
+ }
+ class ComponentThatRenders extends Component {
+ componentDidCatch(error) {}
+ render(props) {
+ return ;
+ }
+ }
+ class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { throwError: true };
+ }
+ componentDidCatch(error) {
+ this.setState({ throwError: false });
+ }
+ render(props, { throwError }) {
+ return throwError
+ ?
+ : ;
+ }
+ }
+ class App extends Component {
+ componentDidCatch(error) {}
+ render(props) {
+ return ;
+ }
+ }
+
+ spy(ComponentThatThrows.prototype, 'componentWillMount');
+ spy(ComponentThatThrows.prototype, 'componentDidCatch');
+ spy(ComponentThatThrows.prototype, 'render');
+ spy(ComponentThatRenders.prototype, 'componentDidCatch');
+ spy(ComponentThatRenders.prototype, 'render');
+ spy(ErrorBoundary.prototype, 'componentDidCatch');
+ spy(ErrorBoundary.prototype, 'render');
+ spy(App.prototype, 'componentDidCatch');
+ spy(App.prototype, 'render');
+
+ render();
+
+ // ComponentThatThrows
+ expect(ComponentThatThrows.prototype.componentDidCatch)
+ .to.not.have.been.called;
+
+ expect(ComponentThatThrows.prototype.componentWillMount)
+ .to.have.been.calledOnce
+ .and.to.throw(Error, ERROR_MESSAGE);
+
+ expect(ComponentThatThrows.prototype.render)
+ .to.not.have.been.called;
+
+ // ComponentThatRenders
+ expect(ComponentThatRenders.prototype.componentDidCatch)
+ .to.not.have.been.called;
+
+ expect(ComponentThatRenders.prototype.render)
+ .to.have.been.calledOnce
+ .and.to.not.throw();
+
+ // ErrorBoundary
+ expect(ErrorBoundary.prototype.componentDidCatch)
+ .to.have.been.calledOnce
+ .and.calledWithExactly(THE_ERROR);
+
+ expect(ErrorBoundary.prototype.render)
+ .to.have.been.calledTwice;
+
+ // App
+ expect(App.prototype.render)
+ .to.have.been.calledOnce;
+
+ expect(App.prototype.componentDidCatch)
+ .to.not.have.been.called;
+ });
+
+ it('should invoke ErrorBoundary\'s componentDidCatch from an error thrown in ComponentThatThrows\' render', () => {
+ const ERROR_MESSAGE = 'render error',
+ THE_ERROR = new Error(ERROR_MESSAGE);
+ class ComponentThatThrows extends Component {
+ componentDidCatch(error) {}
+ render(props) {
+ throw THE_ERROR;
+ return ; // eslint-disable-line
+ }
+ }
+ class ComponentThatRenders extends Component {
+ componentDidCatch(error) {}
+ render(props) {
+ return ;
+ }
+ }
+ class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { throwError: true };
+ }
+ componentDidCatch(error) {
+ this.setState({ throwError: false });
+ }
+ render(props, { throwError }) {
+ return throwError
+ ?
+ : ;
+ }
+ }
+ class App extends Component {
+ componentDidCatch(error) {}
+ render(props) {
+ return ;
+ }
+ }
+
+ spy(ComponentThatThrows.prototype, 'componentDidCatch');
+ spy(ComponentThatThrows.prototype, 'render');
+ spy(ComponentThatRenders.prototype, 'componentDidCatch');
+ spy(ComponentThatRenders.prototype, 'render');
+ spy(ErrorBoundary.prototype, 'componentDidCatch');
+ spy(ErrorBoundary.prototype, 'render');
+ spy(App.prototype, 'componentDidCatch');
+ spy(App.prototype, 'render');
+
+ render();
+
+ // ComponentThatThrows
+ expect(ComponentThatThrows.prototype.componentDidCatch)
+ .to.not.have.been.called;
+
+ expect(ComponentThatThrows.prototype.render)
+ .to.have.been.calledOnce
+ .and.to.throw(Error, ERROR_MESSAGE);
+
+ // ComponentThatRenders
+ expect(ComponentThatRenders.prototype.componentDidCatch)
+ .to.not.have.been.called;
+
+ expect(ComponentThatRenders.prototype.render)
+ .to.have.been.calledOnce
+ .and.to.not.throw();
+
+ // ErrorBoundary
+ expect(ErrorBoundary.prototype.componentDidCatch)
+ .to.have.been.calledOnce
+ .and.calledWithExactly(THE_ERROR);
+
+ expect(ErrorBoundary.prototype.render)
+ .to.have.been.calledTwice;
+
+ // App
+ expect(App.prototype.render)
+ .to.have.been.calledOnce;
+
+ expect(App.prototype.componentDidCatch)
+ .to.not.have.been.called;
+ });
+ });
});