Logging errors is an important aspect of writing real-world applications. When something goes wrong at runtime it's very helpful to have a log detailing what went wrong in order to fix the problem. This is a straightforward process when we're working on the backend code. We can catch the exception and log it along with the stack trace. However, we need to get a bit more creative in order to handle client-side errors.

In this post we'll take a look at propagating errors from a Reagent based app back to the server. A naive implementation might look something like the following. We'll write a function that accepts an event containing the error, then send the error message along with the stack trace to the server:

If we pop up the console in the browser we should see something like the following there:

Uncaught Error: I'm an error
at app.core.home_page (core.cljs:25)
at Object.ReactErrorUtils.invokeGuardedCallback (react-dom.inc.js:9073)
at executeDispatch (react-dom.inc.js:3031)
at Object.executeDispatchesInOrder (react-dom.inc.js:3054)
at executeDispatchesAndRelease (react-dom.inc.js:2456)
at executeDispatchesAndReleaseTopLevel (react-dom.inc.js:2467)
at Array.forEach (<anonymous>)
at forEachAccumulated (react-dom.inc.js:15515)
at Object.processEventQueue (react-dom.inc.js:2670)
at runEventQueueInBatch (react-dom.inc.js:9097)

This gives us the namespace and the line number in the ClojureScript source that caused the error. However, if we print the message that we received on the server it will look as follows:

Error: I'm an error
at app.core.home_page (http://localhost:3000/js/out/app/core.js:51:8)
at Object.ReactErrorUtils.invokeGuardedCallback (http://localhost:3000/js/out/cljsjs/react-dom/development/react-dom.inc.js:9073:16)
at executeDispatch (http://localhost:3000/js/out/cljsjs/react-dom/development/react-dom.inc.js:3031:21)
at Object.executeDispatchesInOrder (http://localhost:3000/js/out/cljsjs/react-dom/development/react-dom.inc.js:3054:5)
at executeDispatchesAndRelease (http://localhost:3000/js/out/cljsjs/react-dom/development/react-dom.inc.js:2456:22)
at executeDispatchesAndReleaseTopLevel (http://localhost:3000/js/out/cljsjs/react-dom/development/react-dom.inc.js:2467:10)
at Array.forEach (<anonymous>)
at forEachAccumulated (http://localhost:3000/js/out/cljsjs/react-dom/development/react-dom.inc.js:15515:9)
at Object.processEventQueue (http://localhost:3000/js/out/cljsjs/react-dom/development/react-dom.inc.js:2670:7)
at runEventQueueInBatch (http://localhost:3000/js/out/cljsjs/react-dom/development/react-dom.inc.js:9097:18)

The stack trace is there, but it's no longer source mapped. So we'll know what namespace caused the error, but not the line in question. In order to get a source mapped stack trace we'll have to use a library such as stacktrace.js. Unfortunately, we won't be able to use the new :npm-deps option in the ClojureScript compiler. This works as expected when :optimizations are set to :none, but fails to provide us with the source mapped stack trace in the :advanced mode.

We need to specify the name of the source map file when using the advanced optimization, tell the compiler to infer the externs, and optionally suppress the warnings.

The new version of the report-error! function will look similar to the original, except that we'll now be passing the error to the StackTrace.fromError function. This function returns a promise containing the source mapped stack trace that we'll be sending to the server:

We can see that the error occurred on line 27 of the app.core namespace which is indeed where the code that throws the exception resides. The full listing for the example is available on GitHub.

While the example in this post illustrates bare bones exception handling, we can do more interesting things in a real world application. For example, re-frame based application could send the entire state of the re-frame database at the time of the error to the server. This allows us to put the application in the exact state that caused the error when debugging the problem.