At ClassDojo, we’ve been working on getting visibility into bugs that get deployed to our production React webapp. Catching errors in our data layer is pretty straightforward, but the view layer presents more of a challenge.

Errors in your React code can happen in a variety of places such as

render functions

functions lifecycle methods ( componentDidMount , componentWillUpdate )

, ) event callbacks ( onClick , onChange )

Our goal was to cover all of these cases without adding boilerplate to every component.

Option A: window.onerror

window.onerror is the easiest way of catching all errors that happen in the webapp. However, certain browsers – most notably Edge, IE < 11, and Safari – don’t pass the actual Error object to the handler, meaning stack traces are inaccessible.

Option B: wrap everything in a try/catch

Using try/catch is the only way to get stack traces in all browsers. Doing this manually on every React component method and event callback is too tedious. Luckily, React provides a hook to modify its execution called a batching strategy.

BatchingStrategy

A BatchingStrategy controls when and how React update code executes, and can have powerful implications on the performance of your React app. For example, Pete Hunt wrote an experimental batching strategy that waits until the next browser animation frame to flush DOM mutations.

Random sidenote: here’s a little snippet on how the default batching strategy came to be:

…when we originally launched React in open source, every setState would immediately trigger a flush to the DOM. That wasn’t part of the contract of setState, but that was just our strategy and it worked pretty well. Then this totally awesome open source contributor Ben Alpert at Khan Academy built a new batching strategy which would basically queue up every single DOM update and state change that happened within an event tick and would execute them in bulk at the end of the event tick.

– React Community Roundup #8

Practically, this means that most of React’s execution, from render functions to event handlers, can be delegated to us. When we continue React’s execution, we can wrap a try/catch around it.

ReactTryCatchBatchingStrategy

Here’s what it looks like:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import ReactUpdates from "react-dom/lib/ReactUpdates" ; import ReactDefaultBatchingStrategy from "react-dom/lib/ReactDefaultBatchingStrategy" ; let isHandlingError = false ; const ReactTryCatchBatchingStrategy = { // this is part of the BatchingStrategy API. simply pass along // what the default batching strategy would do. get isBatchingUpdates () { return ReactDefaultBatchingStrategy . isBatchingUpdates ; }, batchedUpdates (... args ) { try { ReactDefaultBatchingStrategy . batchedUpdates (... args ); } catch ( e ) { if ( isHandlingError ) { // our error handling code threw an error. just throw now throw e ; } isHandlingError = true ; try { // dispatch redux action notifying the app that an error occurred. // replace this with whatever error handling logic you like. store . dispatch ( appTriggeredError ( e )); } finally { isHandlingError = false ; } } }, }; ReactUpdates . injection . injectBatchingStrategy ( ReactTryCatchBatchingStrategy );

A few things of note in the code:

In this implementation, we simply wrap the default React batching strategy. You can further modify the batching strategy if you like.

The error handling code could result in another error during React execution. Therefore, we keep track of error handling state to prevent infinite loops.

For React < 15.4, import React dependencies from "react/lib" instead of "react-dom/lib"

And one caveat to keep in mind: React >= 15 swallows and rethrows errors internally when NODE_ENV === "development" so this batching strategy won’t actually make a difference in dev environments. React contributors did this to ensure exceptions get surfaced to the console.