Handle failures without derailing your code or getting stuck in traffic.
[dev.nathantuggy/railjure "0.1.0"]
This is a reimagining of the classic Railway-Oriented Programming (and its Clojure-specific cousin "Good-Enough error handling in Clojure"), but with some insights from a couple of years of using various previous libraries along the same lines (Railway-Oriented Clojure, Failjure by the author of the second blog post above), as well as some investigation of other libraries such as Promenade and merr.
All of the libraries I’ve looked at, and especially those I’ve used in practice, violate several of these principles, sometimes egregiously. And these violations have led to unsatisfactory code health and teamwork, even at a small scale.
A perhaps surprising consequence of these is my decision to defer writing top-level threading macros along the lines of ->
and ->>
. It’s not because threading is a bad pattern to support, but every library has those whole-fn macros, and they always add complexity overall, because they don’t interoperate with any other threading macros or syntax rewriting macros properly. Promenade shows the limits of this approach clearly with a combinatorial explosion of nine new threading macros, which would need even more to interoperate with other semantic macros!
Instead, Railjure is designed with the assumption that you will be using its macros on a more granular level, typically around each form in a thread. This allows flexibility and makes repeating patterns visually clearer.
Syntax sugar around some common higher-level patterns is fairly likely at some point, once the needs are clearer.
Most names are chosen so you can :refer
them in without confusion.
(:require
[railjure.core as railjure :refer [! fail | ||]])
Use |
("conveyor", for assembly-line transformation of individual values) for a basic failure wrapper in thread-first-ish contexts (at every step), including a failure recovery fn that is called on failure. Use ||
("railway", for bulk processing of collections) for thread-last-ish contexts. Both of these catch exceptions and treat them like failure return values, as well as bypassing execution of the contained, threaded form if its value is a failure already.
Use !
as a recovery fn to just pass the failure along. You can also write your own recovery fns, either in-place or shared. These take three args, the failure itself, the original value that was threaded in, and a fn that can be called nullary to simply retry the original, or unary to pass in a replacement value.
Adapting a simple example from Promenade, suppose you have a database ID, a fast but fallible lookup, a fallback lookup, some sort of transformation operation, and a way to write back to the database.
;; The traditional way
(let [entity (try
(lookup id)
(catch Exception _ (alternate-lookup id)))
transformed (transform entity)]
(try
(write-to-db entity)
(catch Exception e
(errorf e "Failed to save resulting value"
(pr-str transformed)))))
;; The Railjure way (at present)
(-> id
(| lookup
(fn [_failure id _body] (alternate-lookup id)))
(| transform
!)
(| write-to-db
(fn [failure transformed _body]
(errorf failure "Failed to save resulting value: %s"
(pr-str transformed)))))
;; The Railjure way (likely future)
(-> id
(| lookup
(? alternate-lookup id))
(| transform
!)
(| write-to-db
(failure-logf "Failed to save resulting value: %s")))
let
BlocksSometimes there’s no single linear data flow between code sections, but there are still semantic or performance reasons to fail fast. The ok-let
macro works like when-let
, except with as many binding pairs as you like, and checking only for failures at each step. It also wraps the body and all bindings to turn exceptions into failures.
The above example can also be written this way with a few extra features that non-linearity allows:
(ok-let [entity (| id lookup
(fn [_failure id _body] (alternate-lookup id)))
transformed (transform entity)]
(| transformed write-to-db
(fn [failure transformed _body]
(errorf failure
"Failed to save transformed value:%n%s%n(was: %s)"
(pr-str transformed) (pr-str entity))
;; Returns failure to ensure short-circuiting
failure))
;; Returns resulting value as written to the DB
transformed)
The same planned changes apply to the two custom recovery forms, with minor adjustments.
This library should already have decent clj-kondo integration. I’ve also put some effort into auto-indentation (:style/indent
), but my preferred editor (VS Code + Calva using cljfmt-compatible configuration) does not support libraries extending indentation rules yet. Anyone using cljfmt or compatible can add a cljfmt.edn
file at the root of their project looking something like this:
{;; Defaults
;:remove-surrounding-whitespace? true
;:remove-trailing-whitespace? true
;:remove-consecutive-blank-lines? false
;:insert-missing-whitespace? true
;:indent-line-comments? true
;:remove-multiple-non-indenting-spaces? false
:extra-indents
{railjure.core/ok-let [[:block 1]]
;; HACK: Workaround for Calva's limited cljfmt interop
;; See https://github.com/BetterThanTomorrow/calva/issues/2772
;; In the meantime, somewhat over-broadly hits anything with the same name
ok-let [[:block 1]]}}
Bug reports and feature requests, even for fiddly details like editor/linter support, are quite welcome! This is a very small, focused library, so please hold off on starting any PRs until you’ve discussed a plan with me.
Copyright © 2025 Nathan Tuggy
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close