-
-
Notifications
You must be signed in to change notification settings - Fork 716
Debugging Event Handlers
This document is out of date.
A more modern version can be found within the repo
This page describes useful techniques for debugging re-frame's event handlers.
Event handlers are fairly central to a re-frame app. Only event handlers can update app-db
, to "step" an application "forward" from one state to the next.
Approx three kinds of bugs happen in event handlers:
- an exception gets thrown
- a handler does not update
app-db
correctly - a handler corrupts
app-db
Under the covers, re-frame uses a core.async
go-loop
when handling events, and that poses a problem.
If an event handler throws an exception, it will bubble up through the go-loop on its way to the unhandled exception handler. The go-loop
catches the exception, performs some cleanup, and then rethrows it.
This catch and rethrow business causes mayhem when it comes to the correct reporting of stacktraces. Chrome 44 does a good job, but Chrome 42 is hopeless at showing rethrown exceptions.
So, to get an informative stack on Chrome 42, you'll need to intercept any exception thrown within an event handler, and report it before it disappears into the core.async sausage machine.
To that end, here's some helpful middleware log-ex
:
(defn log-ex
[handler]
(fn log-ex-handler
[db v]
(try
(handler db v) ;; call the handler with a wrapping try
(catch :default e ;; ooops
(do
(.error js/console e.stack) ;; print a sane stacktrace
(throw e))))))
If this is complete double dutch to you, then there are other Wiki pages that show how to use and write middlewares.
How do I add this log-ex
middleware to a handler?
Use the 3-arity version of register-handler:
(register-handler
:my-event-id
log-ex ;; <---- middleware for my-handler-fn !!!
my-handler-fn)
Now, every time my-handler-fn
throws, I see a sane stacktrace printed to console.
We'd want to have this middleware on every handler, right? Certainly in development. But, in production we might instead want to send the exception to someone like Airbrake.
So our registration might look like this:
(register-handler
:my-event-id
(if ^boolean goog.DEBUG log-ex log-ex-to-airbrake) ;; alternative middleware
my-handler-fn)
goog.DEBUG
is a compile time constant, set to false
when :optimizations
is :advanced
, otherwise true
. It is supplied by the Google Closure compiler.
Although
log-ex
is no longer really needed (post Chrome 42), I've left it in here because doing so helps the narrative (of having multiple middlewares) below. So take this first step with a grain of salt, and move on.
You might wonder: is my handler making the right changes to app-db
?
The built-in debug
middleware can be helpful in this regard. It shows, via console.log
:
- the event, for example:
[:attempt-world-record true]
- the
db
changes made by the handler in processing the event.
Regarding point 2, debug
uses clojure.data/diff
to compare the state of db
before and after the handler ran. If you look at the docs, you'll notice that diff
returns a triple, the first two of which will be displayed by debug
in console.log
(the 3rd is not interesting).
So, now we have two middlewares to put on every handler: debug
and log-ex
.
At the top of our handlers.cljs
we might define:
(def standard-middlewares [log-ex debug])
And then include this standard-middlewares
in every handler registration below:
(register-handler
:some-id
standard-middlewares ;; <---- here!
some-handler-fn)
No, wait. I don't want this debug
middleware hanging about in my production version, just at develop time. And we still need those runtime exceptions going to airbrake.
So now, we make it:
(def standard-middlewares [ (if ^boolean goog.DEBUG log-ex log-ex-to-airbrake)
(when ^boolean goog.DEBUG debug)])
Ha! I see a problem, you say. In production, that when
is going to leave a nil
in the vector. No problem. re-frame filters out nils.
Ha! Ha! I see another problem, you say. Some of my handlers have other middleware. One of them looks like this:
(register-handler
:ev-id
(path :todos) ;; <-- already has middleware
todos-fn)
How can I add this standard-middlewares
where there is already middleware?
Like this:
(register-handler
:ev-id
[standard-middlewares (path :todos)] ;; <-- put both in a vector
todos-fn)
But that's a vector in a vector? Surely, that a problem?. Actually, no, re-frame will flatten
any level of vector nesting, and remove nils
before composing the resulting middleware.
I'd recommend always having a schema for your app-db
, specifically a Prismatic Schema. If ever herbert is ported to clojurescript, it might be a good candidate too but, for the moment, a Prismatic Schema.
Schemas serve as invaluable documentation, plus ...
Once you have a schema for your app-db
, you can check it is valid at any time. The most obvious time to recheck the integrity of app-db
is immediately after a handler has changed it. In effect, we want to recheck after any handler has run.
Let's start with a schema and a way to validate a db against that schema. I would typically put this stuff in db.cljs
.
(ns my.namespace.db
(:require
[schema.core :as s]))
;; As exactly as possible, describe the correct shape of app-db
;; Add a lot of helpful comments. This will be an important resource
;; for someone looking at you code for the first time.
(def schema
{:a {:b s/Str
:c s/Int}
:d [{:e s/Keyword
:f [s/Num]}]})
(defn valid-schema?
"validate the given db, writing any problems to console.error"
[db]
(let [res (s/check schema db)]
(if (some? res)
(.error js/console (str "schema problem: " res)))))
Now, let's organise for our app-db
to be validated against the schema after every handler. We'll use the built-in after
middleware factory:
(def standard-middlewares [(if ^boolean goog.DEBUG log-ex log-ex-to-airbrake)
(when ^boolean goog.DEBUG debug)
(when ^boolean goog.DEBUG (after db/valid-schema?))]) ;; <-- new
BTW, we could have written it without vectors, using comp
:
(def standard-middlewares (if ^boolean goog.DEBUG ;; not a vector
(comp log-ex debug (after db/valid-schema?)) ;; comp used
log-ex-to-airbrake))
Now, the instant a handler messes up the structure of app-db
you'll be alerted. But this overhead won't be there in production.
These 3 steps will go a very long way in helping you to debug your event handlers.
Deprecated Tutorials:
Reagent: