Liking cljdoc? Tell your friends :D

railjure

Clojars Project

Handle failures without derailing your code or getting stuck in traffic.

[dev.nathantuggy/railjure "0.1.0"]

Design

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.

Principles and Goals

  • Make error handling hard to ignore but easy to skim past
  • Lean into Clojure idioms such as nil-punning, reliance on just a few basic data types, etc
  • Don’t minimize characters — minimize complexity and rigidity
  • Maximize compatibility with ordinary threading macros and syntactic patterns, even third-party where practical
  • Maximize compatibility with existing error-handling patterns/libraries
    • Exceptions, including Slingshots
    • Anomaly maps
    • Failjure
    • ROC
    • Promenade
  • Default to handling all failures, rather than requiring separate code for exceptions, failures, etc
  • Allow explicitly handling specific failures differently in concise and flexible ways
  • Include a fast and obvious native failure type

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.

Usage

Most names are chosen so you can :refer them in without confusion.

(:require
  [railjure.core as railjure :refer [! fail | ||]])

In Threading Macros

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")))

For Complex Data Flows in let Blocks

Sometimes 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.

Editor/Tooling Integration

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]]}}

Contributing

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.

License

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