noun
a means of connection; tie; link.
The UI is a pure function of application state — but event handling can be messy. It doesn’t have to be. Good event handling is declarative, minimal, and keeps side-effects well contained.
Nexus is a small, zero-dependency library for dispatching actions — data describing what should happen — with mostly pure functions.
no.cjohansen/nexus {:mvn/version "2026.06.2"}
Replicant provides a data-driven and functional solution to rendering. By making event handlers representable as data, it provides you with just enough infrastructure to build a declarative action dispatch system. Nexus is designed to be that system.
Here's a compact showcase of using Nexus with Replicant. Read on for a detailed introduction to how it works.
(require '[nexus.registry :as nxr])
(require '[replicant.dom :as r])
(defn save [_ctx system path value]
(swap! system assoc-in path value))
(defn increment [state path]
[[:effects/save path (+ (:step state) (get-in state path))]])
(defn render [state]
[:div
[:p "Number: " (:number state)]
[:div
[:label "Step size: "]
[:input
{:value (:step state)
:on
{:input
[[:effects/save [:step] [:fmt/number [:event.target/value]]]]}}]]
[:button.btn
{:on {:click [[:actions/inc [:number]]]}}
"Count!"]])
;; Handle user input: register effects, actions and placeholders.
;; If you don't like registering these globally, the next section
;; shows how to use nexus.core, which has no implicit state.
(nxr/register-effect! :effects/save save)
(nxr/register-action! :actions/inc increment)
(nxr/register-placeholder! :event.target/value
(fn [{:replicant/keys [dom-event]}]
(some-> dom-event .-target .-value)))
(nxr/register-placeholder! :fmt/number
(fn [_ value]
(or (some-> value parse-long) 0)))
;; The `system`, a mutable application object
(def my-app (atom {}))
;; Specify how an immutable value is derived from the system
(nxr/register-system->state! deref)
;; Wire up the render loop
(r/set-dispatch! #(nxr/dispatch my-app %1 %2))
(add-watch my-app ::render #(r/render js/document.body (render %4)))
;; Trigger the initial render
(reset! my-app {:number 0, :step 1})
Nexus actions are data structures that describe what your system should do.
Actions can originate from user events, timers, network responses, or other sources.
Actions are vectors of an action type (keyword) and optional arguments:
[:task/set-status "tid33" :status/in-progress]
When you dispatch an action in Nexus, it is handled by either:
If the action is handled by an effect handler, we call it an effect.
Nexus makes no assumptions about what the system is—you pass it when
dispatching actions. In the example above, we use the my-app atom.
Let's consider a kanban task tracking example app.
Put effect handlers in your nexus map:
(def nexus
{:nexus/effects
{:task/start-editing
(fn [_ system task-id]
(swap! system assoc-in [:tasks task-id :task/editing?] true))}})
When the action is triggered, e.g. by a DOM event, dispatch it with the nexus
map and your system:
(require '[nexus.core :as nexus])
(def kanban-app (atom {:tasks [,,,]}))
(nexus/dispatch nexus kanban-app {} [[:task/start-editing "tid33"]])
This separates what happens from how, but we can do better—by separating pure logic from effectful execution.
In Kanban, there are limits to how many tasks you can have in each
column at the same time, meaning that the :task/set-status action is
not just a mere assoc-in. This action needs to check how many tasks
we already have with the desired status, and either update the task or
flag an error:
(defn get-tasks-by-status [state status]
(->> (vals (:tasks state))
(filter (comp #{status} :task/status))))
(defn get-status-limit [state status]
(get-in state [:columns status :column/limit]))
(def nexus
{:nexus/effects
{:task/start-editing
(fn [_ system task-id]
(swap! system assoc-in [:tasks task-id :task/editing?] true))
:task/set-status
(fn [_ system task-id status]
(if (< (count (get-tasks-by-status @system status))
(get-status-limit @system status))
(swap! system assoc-in [:tasks task-id :task/status] status)
(swap! system assoc :errors [:errors/at-limit status])))}})
Holy swap, Batman! That's a lot of side-effects in one place. Our goal is to isolate logic in pure functions, so we can test, reuse, and compose behavior without changing the world. Let's fix that.
We will first replace our two effect handlers with a more generic effect handler that updates the application state:
(def nexus
{:nexus/effects
{:effects/save
(fn [_ctx system path v]
(swap! system assoc-in path v))}})
Then our two domain-specific actions, :task/start-editing and
:task/set-status, can now be expressed in terms of this one low-level effect.
We do that by implementing them as action handlers, which are pure functions that return a list of actions.
Action handlers are called with an immutable snapshot of your system, which
means we need to tell Nexus how to acquire the system snapshot. Since our
system is an atom, deref will do the job just fine:
(def nexus
{:nexus/system->state deref ;; <==
:nexus/effects {,,,}
:nexus/actions ;; <==
{:task/start-editing
(fn [state task-id]
[[:effects/save [:tasks task-id :task/editing?] true]])
:task/set-status
(fn [state task-id status]
(if (< (count (get-tasks-by-status state status))
(get-status-limit state status))
[[:effects/save [:tasks task-id :task/status] status]]
[[:effects/save [:errors] [:errors/at-limit status]]]))}})
Now we also have clean separation between pure business logic and the side-effects. Your app will only ever need a handful of effect handlers; as your app grows you'll be adding action handlers. Nothing but pure functions all the way, baby!
It's also worth noting that the data the UI dispatches hasn't changed. We still dispatch something like
[:task/set-status "tid33" :status/in-progress]
but instead of a hard-to-test effect handler responding to this data, we now have an easier-to-test, pure action handler which responds to it by yielding an effect like
[:effects/save [:tasks "tid33" :task/status] :status/in-progress]
This is what allows you to start small and grow your system on demand, with very little boilerplate.
When using Replicant event handler data, you include actions in the rendered hiccup. However, some actions rely on data that isn't available until they dispatch.
Consider this action that updates the task title:
[:input
{:placeholder "Task title"
:name "task/title"
:on {:input [[:task/update-title task-id ???]]}}]
^^^
To dispatch this we need the value from the input field at the time of
dispatch. Placeholders solve this problem in a declarative way:
[:input
{:placeholder "Task title"
:name "task/title"
:on {:input [[:task/update-title task-id [:event.target/value]]]}}]
[:event.target/value] is a placeholder to be resolved during dispatch.
Placeholders are implemented by the keyword:
(def nexus
{:nexus/system->state deref
:nexus/effects {,,,}
:nexus/actions {,,,}
:nexus/placeholders ;; <==
{:event.target/value
(fn [dispatch-data]
(some-> dispatch-data :dom-event .-target .-value))}})
Where does dispatch-data come from? It is the third argument to
nexus.core/dispatch:
(nexus/dispatch nexus system {:dom-event ,,,}
[[:task/update-title "tid33" [:event.target/value]]])
You can also embed dispatch data in the state snapshot by replacing
:nexus/system->state with :nexus/system+dispatch-data->state. This function
will receive both the system and dispatch data, and is expected to return an
immutable state snapshot.
Dispatch data is also available to effect handlers. Let's say we have a form to
edit the task. Instead of controlling each input, it will use the form submit
action. To avoid a page refresh on submit we must call .preventDefault on the
event:
[:form
{:on
{:submit
[[:effects/prevent-default]
[:task/edit task-id [:event.target/form-data]]]}}
,,,]
Effect handlers receive dispatch-data as part of their first argument, the
aforementioned context map:
(def nexus
{,,,
:nexus/effects
{,,,
:effects/prevent-default
(fn [{:keys [dispatch-data]} _]
(some-> dispatch-data :dom-event .preventDefault))}})
You may want to record the time of the last edit. However, getting the current time in the action handler means it's no longer pure (and will make it much harder to test). You can solve this by passing in the current time.
One way to achieve this is to use another placeholder:
(def nexus
{,,,
:nexus/placeholders
{,,,
:clock/now
(fn [{:keys [now]}]
now)}})
(nexus/dispatch nexus system {:dom-event ,,,
:now (js/Date.)}
[[:task/update-title "tid33" [:event.target/value] [:clock/now]]])
Note that you could write this placeholder very succinctly:
(def nexus
{:nexus/placeholders
{:clock/now :now}})
Another option is to make sure the state always has the current time on it:
(def nexus
{,,,
:nexus/system->state
(fn [system]
(assoc @system :clock/now (js/Date.)))
:nexus/actions
{:task/edit
(fn [state task-id data]
(into [[:effects/save [:tasks task-id :task/updated-at] (:clock/now state)]]
(for [[k v] data]
[:effects/save [:tasks task-id k] v])))}})
You may have wondered why the placeholder keyword is wrapped in a vector:
[:event.target/value]. The vector allows placeholders to nest, so you can
transform late bound values without having to litter your code with things like
parse-long, or duplicate placeholders, e.g. :event.target/value-as-*.
Let's say you wanted to get a number from an input field:
[:input
{:on
{:input
[[:task/set-order task-id [:fmt/long [:event.target/value]]]]}}]
The :fmt/long placeholder takes an argument, which is passed to the
placeholder function:
(def nexus
{,,,
:nexus/placeholders
{:event.target/value
(fn [{:keys [dom-event]}]
(some-> dom-event .-target .-value))
:fmt/long
(fn [_ val]
(or (some-> val parse-long) 0))}})
If you have actions with large data structures in them that should never contain placeholders, you can opt out of interpolation for some performance benefits by adding meta data:
[[:task/set-order
(with-meta huge-task {:nexus/skip-interpolation true})
[:fmt/long [:event.target/value]]]]
You may want to consider not putting enormous data structures in actions first, but for those cases where it can't be avoided, this escape hatch is available.
Most of the time, adding ^:nexus/skip-interpolation is not necessary. Measure
before you optimize.
Batching is deprecated, see the dedicated document for details.
Imagine that there is a form to add new tasks. When the form is submitted, we want to issue a command to the server to create a new task, and redirect the user to a dedicated page for it.
Here's an effect handler to send a command to the server:
(def nexus
{:nexus/effects
{:effects/command
(fn [ctx system command]
(js/fetch "/commands"
#js {:method "POST"
:body (pr-str command)}))}})
To redirect the user, we need to dispatch a new effect when the command
completes, looking up the new location from the response. The ctx passed to
effect handlers includes a :dispatch function. It lets you trigger new effects
with access to the same nexus, system and dispatch-data.
(def nexus
{:nexus/effects
{:effects/command
(fn [{:keys [dispatch]} system command]
(-> (js/fetch "/commands"
#js {:method "POST"
:body (pr-str command)})
(.then
(fn [response]
(dispatch
[[:effects/navigate
(.get (.-headers response) "Location")]])))))}})
This works, but it couples concerns more tightly than we'd like. We want as few effects as possible, meaning that they should be as general as possible. We can't assume that every command should result in a navigation. Instead we want to make this decision when issuing the command:
(nexus/dispatch nexus system dispatch-data
[[:effects/command
{:command/kind :commands/create-task
:command/data
{:task/title "Learn Nexus"
:task/priority :task.priority/high}}
{:on-success [[:effects/navigate ???]]}]])
^^^
Once again, we want to refer to a value that’s only available when the effect is
triggered—without writing imperative glue code. Unfortunately we can't just
stick a placeholder here, as it would resolved immediately upon dispatching
:effects/command -- there is no way for Nexus to know that :on-success
describes something that should happen later.
This problem needs a custom solution in your app. One possible solution is to use a placeholder anyway, and implement it such that it preserves the placeholder when there is not yet a value to replace it with:
(def nexus
{:nexus/placeholders
{:http.res/header
(fn [{:keys [response]} header]
(if response
(.get (.-headers response) header)
[:http.res/header header]))}})
We can use the placeholder in the :on-success effects:
[[:effects/command
{:command/kind :commands/create-task
:command/data
{:task/title "Learn Nexus"
:task/priority :task.priority/high}}
{:on-success [[:effects/navigate [:http.res/header "Location"]]]}]]
Finally, in the command effect handler, we can provide additional dispatch data when dispatching new effects. This dispatch data will be merged into the original dispatch data:
(def nexus
{:nexus/placeholders
{:http.res/header
(fn [{:keys [response]} header]
(.get (.-headers response) header))}
:nexus/effects
{,,,
:effects/command
(fn [{:keys [dispatch]} system command {:keys [on-success]}]
(-> (js/fetch "/commands"
#js {:method "POST"
:body (pr-str command)})
(.then
(fn [response]
(when on-success
(dispatch on-success {:response response}))))))}})
The effect handler now accepts a fourth argument that matches the extra options in the dispatched effect:
[:effects/command command opts]
For a production-ready setup you should also support an :on-failure option in
the last map.
With this setup, we’ve cleanly separated concerns: the command effect remains generic, while callers can declaratively define what should happen on success. This pattern generalizes well and can be used to implement all kinds of asynchronous flows.
If you're rendering with Replicant, you can introduce it to Nexus with a one-liner:
(require '[nexus.core :as nexus]
'[replicant.dom :as r])
(def nexus ,,,)
(def system (atom {}))
(defn state->ui-data [state]
[:div
[:h1 "Hello world!"]])
(defn start [el nexus system]
;; Dispatch Replicant's event data with Nexus
(r/set-dispatch! #(nexus/dispatch nexus system %1 %2))
(add-watch system ::render #(r/render el (state->ui-data %4)))
(swap! system assoc ::started-at (js/Date.)))
Replicant calls the function passed to set-dispatch! with a map that contains,
among other things, the DOM event under the key :replicant/dom-event.
Beware that this approach can cause multiple renders for a single dispatch if
there are more than one effect that swaps on the system atom. To ensure each
dispatch only results in a single render (which will give you the best rendering
performance), you have a few options.
If you only need to trigger rendering from a Nexus dispatch, you can use an interceptor:
(defn start [el nexus system]
(let [nexus (update nexus :nexus/interceptors conj
{:after-dispatch
(fn [ctx]
(r/render el (state->ui-data @system))
ctx)})]
(r/set-dispatch! #(nexus/dispatch nexus system %1 %2))
(swap! system assoc ::started-at (js/Date.))
(r/render el (state->ui-data @system))))
This will trigger a render for every dispatch, including async ones triggered by
effects. However, swap!-ing on the atom from elsewhere will not trigger a render.
If you'd rather tie rendering to updates to the system atom, you can implement
a render lock to avoid multiple renders per dispatch. The render lock is
controlled from an interceptor to ensure that it is also in place for async
dispatch calls triggered by effects:
(defn render [el state]
(when-not (:pause-rendering? state)
(r/render el (state->ui-data state))))
(defn start [el nexus system]
(let [nexus (update nexus :nexus/interceptors conj
{:before-dispatch
(fn [ctx]
(swap! system assoc :pause-rendering? true)
ctx)
:after-dispatch
(fn [ctx]
(swap! system dissoc :pause-rendering?)
ctx)})]
(r/set-dispatch! #(nexus/dispatch nexus system %1 %2))
(add-watch system ::render #(render el %4))
(swap! system assoc ::started-at (js/Date.))))
By batching the effects that swap on the system, you ensure that each dispatch only results in at most one swap, thus one render. However, this approach is discouraged and deprecated, see the batching document.
...if only you could see what I've seen with your eyes
If you are using Dataspex you can load Nexus' custom action panel like so:
(require '[nexus.action-log :as action-log])
(defn inspect-actions [nexus]
(let [log (action-log/create-log)]
(action-log/install-inspector log)
(action-log/install-logger nexus log)))
...and use the returned nexus with nexus/dispatch.
(nexus.action-log/create-log & [opt])You can pass some options to this function to customize the log:
:slow-threshold The number of milliseconds used to determine if a dispatch
was slow. Defaults to 100.:max-entries The maximum number of entries to render. Will hide the oldest
dispatches. Setting this to a reasonable number will keep the inspector
snappy.:max-age A map of {:seconds :minutes :hours :days} that determines how old
an action should be before it is no longer rendered in the log. For instance,
(action-log/create-log {:max-age {:hours 1}}) will only render actions from
the past hour. Can be used in combination with :max-entries.Compiling the nexus map and passing it by hand makes things explicit at the
cost of some boilerplate on your end. Maybe you're willing to swallow a little
implicitness for a lot of convenience? Then nexus.registry is for you.
nexus.registry provides some wrappers that collect effect handlers,
action handlers, and placeholders in a global registry:
(require '[nexus.registry :as nxr])
(nxr/register-effect! :effects/save
^:nexus/batch
(fn [_ctx system path-vs]
(swap! system
(fn [state]
(reduce (fn [acc [path v]]
(assoc-in acc path v))
state path-vs)))))
(nxr/register-placeholder! :event.target/value
(fn [{:replicant/keys [dom-event]}]
(some-> dom-event .-target .-value)))
(nxr/register-placeholder! :fmt/number
(fn [_ val]
(or (some-> val parse-long) 0)))
(nxr/register-action! :task/start-editing
(fn [state task-id]
[[:effects/save [:tasks task-id :task/editing?] true]]))
(nxr/register-action! :task/set-status
(fn [state task-id status]
(if (< (count (get-tasks-by-status state status))
(get-status-limit state status))
[[:effects/save [:tasks task-id :task/status] status]]
[[:effects/save [:errors] [:errors/at-limit status]]])))
,,,
nexus.registry/dispatch implicitly uses this registry:
(replicant.dom/set-dispatch!
(fn [dispatch-data actions]
(nxr/dispatch system dispatch-data actions)))
Using the global registry makes the dev setup much easier, because you don't
need to coordinate on the nexus map. Just add this to your development setup:
(require '[nexus.action-log :as action-log])
(action-log/inspect {:max-age {:hours 3}})
See above for details on the optional map passed to inspect.
Tutorials on the Replicant website demonstrate that rolling your own action dispatch system is not a big undertaking. So why does this library exist?
Designing a robust action dispatch system can be daunting when you’re just getting started. Even if the pieces are conceptually simple, it takes time and experience to get the architecture right, especially in a functional setting.
We’ve spent over a decade building systems like this in Clojure. Nexus is our distilled take on how to do it cleanly and consistently, so you don’t have to reinvent the wheel or get stuck figuring it out on day one.
Even for experienced developers, it’s easy to cut corners when wiring up yet another dispatch system—skipping instrumentation, half-assing some features, or going light on error handling. Nexus takes care of those finer points once and for all.
Every app needs some kind of effect dispatch system. Even if we all agree that an effect is a vector with a keyword identifier and some arguments, there are many ways to build the dispatch pipeline. There’s little value in making it slightly different in every project—but doing so does come with a cognitive cost.
You need some action system from the very beginning—often before you know if you’ll need to separate actions from side-effects, if actions will need to trigger other actions (maybe asynchronously), or if you’ll want placeholder interpolation.
If we can build a strong, flexible system for this flow in a small package, it can help reduce decision fatigue and free up your mental bandwidth to focus on your domain instead of your wiring.
Sure, a basic dispatch system isn’t much code—but it is code you’ll end up copying between projects. With Replicant and Nexus together, you get up and running with just a handful of lines. That means less setup, fewer errors, and more consistency across projects.
Action dispatch is one of those areas where good observability is a genuine superpower. Nexus ships with built-in integration for Dataspex, letting you inspect dispatch flows live during development.
It also exposes a fine-grained interceptor API you can extend however you want—add production tracing, connect it to custom tooling, or build your own time-travel debugger.
We’re not trying to scare you into using a library when 50 lines of code could work. But for ~160 lines, Nexus gives you a clean, tested, extensible dispatch system with excellent dev tooling built in.
Finding the right nomenclature was the most important part of the Nexus design process. Strong names empower you to build a system with the right focus, help you reach for the right tools for the job, and lightens the learning curve.
A vector beginning with a keyword (e.g. [:auth/login "alice" "secret"]) that
represents an instruction to the system. The keyword identifies the action type,
and any following values are action arguments.
Actions were deliberately not named "events" for two reasons:
Action handlers are pure functions that take the state (see below) and any arguments from the action, and return a list of actions/effects.
When an action reaches an effect handler to be processed for its side-effects, we call it an effect.
An effect handler is a function that receives ctx, the live system and any
action arguments and uses them to perform side-effects:
[:effect/save [:number] 3]
(fn [ctx system path value])
Here path would be bound to [:number] and value to 3.
When an effect handler is batched, it receives a collection of arguments instead:
[[:effect/save [:number] 3]
[:effect/save [:name] "Nexus"]]
^:nexus/batch
(fn [ctx system path-values])
In this case path-values would be:
[[[:number] 3]
[[:name] "Nexus"]]
The system contains your live, mutable application state and services. Nexus
treats this as an opaque value—it never inspects or modifies it. It is passed to
effects (to perform work) and to your :nexus/system->state or
:nexus/system+dispatch-data->state function (to extract pure data for action
handlers).
The result of calling :nexus/system->state or
:nexus/system+dispatch-data->state on your system. The result is assumed to be
immutable data.
Data that is only available at the time an action is dispatched. Prime examples include the DOM event that triggered the action or the target element, but can be anything a pure action handler wouldn't otherwise have access to, like the current time.
Placeholders allow actions to refer to data that isn't available until the action is dispatched. They are represented as a vector with a keyword, and possibly additional arguments. Placeholders have an implementation associated with the first keyword.
nexusA map defining your Nexus system. It includes keys like :nexus/actions,
:nexus/effects, :nexus/placeholders, :nexus/interceptors, and
:nexus/system->state. This is the central registry passed to
nexus.core/dispatch.
When using nexus.registry, Nexus maintains this registry for you.
ctxThe context map passed to interceptors. It contains data relevant to the current
interceptor phase (e.g. :action, :errors, :results) and data about other
interceptors (:queue/:stack). Interceptors can read or modify this context
to influence execution.
Nexus catches errors from handlers and interceptors, and propagates them as data. By default, a dispatch can be partially successful—some actions may complete while others fail in an action or effect handler. You can control this behavior with interceptors. Nexus ships with an opt-in fail fast strategy if you'd like all action and effect processing to halt on the first error:
(require '[nexus.strategies :as strategies])
(def nexus
{:nexus/interceptors [strategies/fail-fast]
:nexus/actions ,,,
:nexus/effects ,,,})
If you're using the registry:
(require '[nexus.registry :as nxr])
(require '[nexus.strategies :as strategies])
(nxr/register-interceptor! strategies/fail-fast)
With the fail-fast strategy, action and effect processing stops at the first
failure. dispatch itself will never throw. Errors are collected and returned
in the :errors key. If you want an exception thrown on error, you can do that
after calling dispatch:
(require '[nexus.core :as nexus])
(let [{:keys [results errors]} (nexus/dispatch nexus actions)]
(when-let [error (->> errors (keep :err) first)]
(throw error)))
Note that this will only throw the first of potentially several errors.
Interceptors let you instrument and control the flow of actions. They can run before or after dispatch, action handlers, and effect handlers.
An interceptor is a map with an :id and any number of phase functions, keyed
by:
:before-dispatch:after-dispatch:before-action:after-action:before-effect:after-effectAll interceptor functions take a single argument, ctx, and must return an
updated ctx. Its contents vary by phase, but always include:
:queue — the remaining before-phase interceptors. The handler function
appears at the end of this list.:stack — the after-phase interceptors to run once the handler completes.By modifying :queue or :stack, interceptors can short-circuit processing or
skip specific interceptors.
Specific arguments passed to the various phase functions:
:before-dispatch:system:state:dispatch-data:actions:after-dispatch:system:state:dispatch-data:actions:results:errors:before-action:before-action is only called for actions that have a registered action
handler. Any action that only has an effect handler will not be visible with
this interceptor.
:state:action:errors (errors from previous action handlers):after-action:after-action is called after an action is expanded, and each resulting
action/effect has been expanded and executed.
:state:actions:errors (errors from previous action handlers):before-effect:system:dispatch-data:dispatch — a function to dispatch new actions with the same nexus,
system, and dispatch-data.:effect or :effects (batched effect handler):errors (errors from previous effect handlers):results (from previous effects):after-effect:system:dispatch-data:dispatch:effect or :effects (batched effect handler):errors:resultsHere's an example of an interceptor that logs actions as they're processed as effects:
(def logger
{:id :logger
:before-action
(fn [{:keys [action] :as ctx}]
(println "Expanding action:\n" (pr-str action))
ctx)
:after-action
(fn [{:keys [errors] :as ctx}]
(when (seq errors)
(println "⚠️ Error while expanding action!")
(prn errors))
ctx)
:before-effect
(fn [{:keys [effect effects] :as ctx}]
(if effect
(println "Executing effect:\n" (pr-str effect))
(println "Executing batched effects:\n" (pr-str effects)))
ctx)
:after-effect
(fn [{:keys [errors] :as ctx}]
(when (seq errors)
(println "⚠️ Error while executing effect!")
(prn errors))
ctx)})
(def nexus
{:nexus/interceptors [logger]
:nexus/actions ,,,
:nexus/effects ,,,})
You can also register interceptors with nexus.registry/register-interceptor!.
You can register a function to receive any error Nexus encounters. The function
will be called with two arguments: the interceptor ctx map, and a map
describing the error. Register this function as :nexus/on-error in your Nexus
map, or with (nexus.registry/on-error (fn [ctx error] ,,,)).
Ring middleware: ring-nexus-middleware applies Nexus to Ring handlers for clean, testable web applications.
Datastar: datastar.wow uses Nexus to create extensible, testable, and data-oriented real-time applications via Datastar.
Original design by Magnar Sveen (@magnars), Christian Johansen (@cjohansen), and Teodor Heggelund (@teodorlu).
Change execution model to eagerly execute commands, see relevant ADR.
Thanks Cormac Cannon!
Deprecate batching, and move it to an opt-in interceptor. See section on batching above.
Remove the error propagation for synchronous nested calls to dispatch
introduced in 2025.11.1. This change duplicated errors in interceptors, and does
not work consistently for async and sync calls to dispatch.
Include a :trace with errors that includes every action and effect that lead
to an error. This gives full traceability across nested dispatch calls for both
async and sync calls, and can be leveraged by tooling to properly "connect the
dots" when errors occur.
Add :nexus/system+dispatch-data->state as an alternative to
:nexus/system->state.
Add ^:nexus/skip-interpolation to skip interpolation of large data structures
in action data.
Add :nexus/on-error callback to observe every error occurring during dispatch.
Fix a bug where nested calls to dispatch would swallow errors. Synchronous
errors are now propagated to the root call. Thanks Martin
Solli!
Fixed a bug with interpolation accidentally introduced in 2025.10.1 (Martin Solli).
Interpolate placeholders in between each action expansion.
Copyright © 2025-2026 Christian Johansen, Magnar Sveen, and Teodor Heggelund. Distributed under the MIT License.
Nexus is software written for and by humans.
Can you improve this documentation? These fine people already did:
Christian Johansen, Kevin J. Lynagh, Ovi Stoica, Grigoriy Beziuk, Martin Solli, Brian Scaturro & Anders FursethEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |