Skip to content

Instantly share code, notes, and snippets.

@Aldredcz
Last active December 10, 2021 10:30
Show Gist options
  • Save Aldredcz/4d63b0a9049b00f54439f8780be7f0d8 to your computer and use it in GitHub Desktop.
Save Aldredcz/4d63b0a9049b00f54439f8780be7f0d8 to your computer and use it in GitHub Desktop.

Notes

  • This code handles any JS runtime error during rendering React components. Without this handling, once an error occurs, whole component tree is damaged and can't be used at all. With this handling, nothing will be rendered in production environment (error span in dev env.) + in production the error is logged to Sentry (if you are not using it just delete related code)
  • This is basicaly a workaround for proposed feature in React core - described in Issue: facebook/react#2461
  • Works for all variants of Component creation - React.createClass, extending React.Component and also stateless functional components.
  • To get this work, just put this snippet into your entry js file. Then it will work in whole application.
  • Also supporting React Hot Reload!
  • If you find this useful, please retweet https://twitter.com/Aldredcz/status/744650159942995968 :)

Ideas

  • specify custom error renderer (global / per component, e.g. by implementing method renderOnError() in a component)

Snippet code

import React from 'react';

const statelessComponentsMap = new Map(); // original -> monkeypatched stateless functional components cache
let errorPlaceholder = <noscript/>;

if (__DEV__) {
	errorPlaceholder = (
		<span
			style={{
				background: 'red',
				color: 'white'
			}}
		>
			Render error!
		</span>
	);
}

function logError(Component, error) {
	const errorMsg = `Error while rendering component. Check render() method of component '${Component.displayName || Component.name || '[unidentified]'}'.`;

	console.error(errorMsg, 'Error details:', error); // eslint-disable-line

	if (typeof Raven !== 'undefined' && typeof Raven.captureException === 'function') {
		Raven.captureException(new Error(errorMsg), {
			extra: {
				errorStack: error.stack
			}
		});
	}
}

function monkeypatchRender(prototype) {
	if (prototype && prototype.render && !prototype.render.__handlingErrors) {
		const originalRender = prototype.render;

		prototype.render = function monkeypatchedRender() {
			try {
				return originalRender.call(this);
			} catch (error) {
				logError(prototype.constructor, error);

				return errorPlaceholder;
			}
		};

		prototype.render.__handlingErrors = true; // flag render method so it's not wrapped multiple times
	}
}

const originalCreateElement = React.createElement;
React.createElement = (Component, ...rest) => {
	if (typeof Component === 'function') {

		if (Component.prototype && typeof Component.prototype.render === 'function') {
			monkeypatchRender(Component.prototype);
		}

		// stateless functional component
		if (!Component.prototype || !Component.prototype.render) {
			const originalStatelessComponent = Component;
			if (statelessComponentsMap.has(originalStatelessComponent)) { // load from cache
				Component = statelessComponentsMap.get(originalStatelessComponent);
			} else {
				Component = (...args) => {
					try {
						return originalStatelessComponent(...args);
					} catch (error) {
						logError(originalStatelessComponent, error);

						return errorPlaceholder;
					}
				};
				
				Object.assign(Component, originalStatelessComponent); // copy all properties like propTypes, defaultProps etc.
				statelessComponentsMap.set(originalStatelessComponent, Component); // save to cache, so we don't generate new monkeypatched functions every time.
			}
		}
	}

	return originalCreateElement.call(React, Component, ...rest);
};


// allowing hot reload
const originalForceUpdate = React.Component.prototype.forceUpdate;
React.Component.prototype.forceUpdate = function monkeypatchedForceUpdate() {
	monkeypatchRender(this);
	originalForceUpdate.call(this);
};
@hyperh
Copy link

hyperh commented Feb 16, 2017

Hmm not working for me. I still get uncaught errors.

@sontek
Copy link

sontek commented Apr 4, 2017

@julien-f your solution works great for me but it blows up with HMR

@lassombra
Copy link

Thanks for providing this. It solved exactly the problem I was dealing with and I was just getting ready to write something like this myself. So having a mostly working one is a godsend.

I did make one change in my version, I added a test for the existence of prototype on the component, as it is true, arrow functions don't have prototype if they are implemented directly in the language (not true for babel). Since I'm using node 6 I have access to native arrow functions, which don't have prototype at least on the server. I also changed one part to ignore components which extend a certain base class, but that is implementation specific.

@Aldredcz
Copy link
Author

Aldredcz commented Jun 1, 2017

@lassombra Thanks for your comment. I thought I've already solved the prototypeless case, but I only did it in one condition.

My bad. Fixed now :)

@elmeister
Copy link

elmeister commented Nov 21, 2018

Any idea why this works only in development and doesn't work in production?
I already removed if (__DEV__) { condition obviously.

@natocTo
Copy link

natocTo commented Aug 11, 2019

If someone still use this. The "allowing hot reload" part should be:

const originalForceUpdate = React.Component.prototype.forceUpdate;
React.Component.prototype.forceUpdate = function monkeypatchedForceUpdate(callback) {
	monkeypatchRender(this);
	originalForceUpdate.call(this, callback);
};

There was no callback argument. We had weird error in React-bootstrap (0.31.5) because of this - missing initial Modal animation but only for some ones. Also React-bootstrap (0.32.5 - latest on Bootstrap 3) not work at all with this ErrorHandler hack, have no clue why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment