-
-
Notifications
You must be signed in to change notification settings - Fork 715
Debugging Event Handlers
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 subtly corrupts
app-db
Under the covers, re-frame uses core.async
and, while it is wonderful, core.async produces a special kind of hell when in comes to stack traces. After an exception passes through a go-loop
an exception's stack is mangled beyond repair and you'll have no idea where the exception was thrown.
So, ... to end up with a useable, informative stack and have some chance of debugging a problem, you'll have to intercept any exception thrown in an event handler, and report it before that exception makes it back back up the call chain and into the core.async sausage machine.
Luckily, we can easily use middleware for that: add log-ex
to your handlers so that exceptions are printed to console before they are mangled beyond repair.
Here's what log-ex
looks like (will be standard in v0.3.0):
(defn log-ex
[handler]
(fn log-ex-handler
[db v]
(try
(handler-fn db v) ;; call the handler with a wrapping try
(catch :default e ;; ooops
(do
(.error js/console e)
(throw e))))))
Complete double dutch to you? There are other Wiki pages herein which show you how to use and write middleware.
How do I add this log-ex
middleware?
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 stack trace 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 airbrake.
So our registration might look like this:
(register-handler
:my-event-id
(if goog.DEBUG log-ex log-ex-to-airbrake)
my-handler-fn)
goog.DEBUG
is effectively a compile time constant set to false
when optimizations
is :advanced
, otherwise true
. But this is nothing to do with re-frame -- it is a closure compiler thing.
You wonder: is my handler making the right changes to app-db
?
The built-in debug
middleware can be helpful in this regard. It shows, on console.log
:
- the event:
[: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 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:
(register-handler
:e-id
standard-middlewares ;; <---- here!
a-handler-fn)
No, wait. I don't want that debug
hanging about at production time, just develop time. And we need those runtime exceptions going to airbrake.
So now, we make it:
(def standard-middlewares [ (if goog.DEBUG log-ex log-ex-to-airbrake)
(when 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 a 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)] ;; <-- both in a vector
todos-fn)
But that's a vec in a vec? Surely, that a problem?. No, re-frame will both flatten the vectors, and remove nils
before composing the middleware you provide.
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 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.
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]))
(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 to run our validation function:
(def standard-middlewares [ (if goog.DEBUG log-ex log-ex-to-airbrake)
(when goog.DEBUG debug)
(when goog.DEBUG (after db/valid-schema?]) ;; <-- new
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.
Those steps will go a very long way to helping you to debug your event handlers.
Deprecated Tutorials:
Reagent: