Catching errors with ClojureScript contracts

When refactoring code, it often happens that it breaks in odd places. Additionally, it may not result in a nice error with a nice stacktrace that we can follow and find the offending place. Instead, we get odd data, odd-looking output, and it leaves us in the blue.

One way to approach this contracts. In Clojure(Script) they’re simply called pre/post conditions, and we can specify them when defining a function with defn form. Whenever the function is called, and the input or output doesn’t satisfy the contract, an assertion error is thrown, and we can inspect the stacktrace.

I found this facility useful when adding Z (vertical) dimension to FPE. It’s got various specialized functions that were giving me the NaNs where I needed numbers.

Below is a snippet that converts a map from FPE representing a wall, into a Wall record. Instead of constantly checking whether it’s a curved wall or not, I use line-segment function that returns either Line or Bezier values. Both of these values have an implementation of PVertexAccess protocol, so when I call (vertices seg), I will get my vertices no matter what kind of segment that is. Polymorphism for the win.




(defn fpe->Wall
"Convert FPE wall (keywordized map) to Wall record."
(let [{:keys [a b c left right]} wall
{a-low :z a-high :h} a
{b-low :z b-high :h} b
{la :a lb :b lc :c} left
{ra :a rb :b rc :c} right]
(line-segment ;; low
[(😡 a) (:y a) a-low]
[(😡 b) (:y b) b-low]
(when c [(😡 c) (:y c) (mix a-low b-low 0.5)]))
(line-segment ;; high
[(😡 a) (:y a) a-high]
[(😡 b) (:y b) b-high]
(when c [(😡 c) (:y c) (mix a-high b-high 0.5)]))
(line-segment ;; left
[(😡 la) (:y la)]
[(😡 lb) (:y lb)]
(when c [(😡 lc) (:y lc)]))
(line-segment ;; right
[(😡 ra) (:y ra)]
[(😡 rb) (:y rb)]
(when c [(😡 rc) (:y rc)]))
(-> wall :thickness)
(-> wall :balance)
(mapv fpe->Opening (-> wall :openings)))))

view raw
hosted with ❤ by GitHub

Now to the contracts, if any of the input data is not a number? or is a NaN I will get a nice error thrown and don’t have to look further into my code, I know it comes from FPE.



(defn- line-segment
[a b c]
{:pre [(every? number? (flatten (if c [a b c] [a b])))
(not (some js/isNaN (flatten (if c [a b c] [a b]))))]
:post [(satisfies? g/PVertexAccess %)]}
(if c
(case (count c)
2 (->> [a c b] (map vec2) dm/qbezier->cbezier b/bezier2)
3 (->> [a c b] (map vec3) dm/qbezier->cbezier b/bezier3))
(case (count a)
2 (l/line2 a b)
3 (l/line3 a b))))

view raw
hosted with ❤ by GitHub

And here we go to dev console:


All assertions in pre/post conditions can, and should be eliminated in the production builds, using compiler options.


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.