FlowStorm: Debugging and Understanding Clojure Code on a New Level - Flexiana
avatar

Khatia Tchitchinadze

Posted on 13th May 2025

FlowStorm: Debugging and Understanding Clojure Code on a New Level

news-paper Hidden |

What is FlowStorm and Why Should You Care? 

Ever found yourself lost in a cascade of println statements, desperately trying to understand how data transforms across your Clojure functions? Or you’ve battled elusive bugs that only surface under specific, hard-to-reproduce conditions? If these scenarios sound familiar, you’re in for a treat.

In a previous article, https://flexiana.com/news/2025/04/why-clojure-developers-love-the-repl-so-much, I mentioned some tools for examining data and code. Since 2023, one of them has become essential in my projects: FlowStorm

Brief Overview of FlowStorm’s Capabilities 

FlowStorm started as a tracing debugger but has evolved into something far more ambitious. Its description as “Much more than a debugger” is apt, as it offers a suite of visualization, interactive exploration, and deep code understanding capabilities that most other programming languages don’t provide. 

Think of it: the ability to effortlessly trace data flows, step through execution, and visualize complex states without peppering your code with temporary debug lines. This is the power FlowStorm brings to your Clojure(Script) development.

Here’s a quick look at its features: 

CLJ / CLJS / Babashka support: FlowStorm works with most Clojure types. Although some features, like automatic setup, work a bit differently on ClojureScript. 

Execution Recording: It records how your Clojure code runs. Usually, you don’t need to change your code or do a lot of setup, especially with ClojureStorm. 

Timeline Navigation: Lets you go through the recorded history of your code. You can step forward and backward or jump to specific spots. 

Data Visualization: Has tools to look at your application’s data as it was when the code ran. It has built-in viewers and you can make your own. 

Code Analysis Tools: Includes things like call-stack trees, function call summaries, and search to help you see how your program ran. 

REPL Integration: FlowStorm and your REPL work together in both directions: you can send values and states from FlowStorm to your REPL, and also access FlowStorm’s recordings and internals from within your REPL. 

Extensibility: Supports plugins and custom tooling (detailed in “Additional Features” section)

FlowStorm can be integrated into your workflow in two ways. The recommended approach is “ClojureStorm,” which swaps your Clojure compiler at dev time for automatic instrumentation. Alternatively, “Vanilla FlowStorm” lets you add FlowStorm to your dev classpath and instrument by tagging and re-evaluating forms. In this tutorial, we’ll walk through the core features of FlowStorm with hands-on examples, from basic setup to advanced debugging techniques.

Your First Project with FlowStorm – An Interactive Tutorial 

Prerequisites 

Before you start: This tutorial expects you have a Clojure setup, know basic Clojure and how to use a REPL. 

In this tutorial I will guide you from the most simple to more advanced features step by step, if you ever get lost or you would like to try a different tutorial, there is own offered by FlowStorm itself and accessible via “Help” -> “Tutorial”. 

Setting up the Environment 

  1. Create a project as: 

mkdir fs-example
cd fs-example
cat << EOF > deps.edn
{:paths ["src/"] ; where your cljd files are
 :deps {quil/quil {:mvn/version "4.3.1563"}}
 :aliases {:dev {:classpath-overrides {org.clojure/clojure nil}
                 :extra-deps {com.github.flow-storm/clojure {:mvn/version "1.12.0-9"}
                              com.github.flow-storm/flow-storm-dbg {:mvn/version "4.4.1"}}}}}
EOF

2. Create a default user namespace: 


mkdir dev
cat << EOF > dev/user.clj
(ns user
  (:require [quil.core :as q]))
EOF

Launching and Initialization 

  1. Launch a REPL with dev alias 
  2. If you use Emacs + Cider, this can be done by pressing C-u C-c M-j and adding :dev to the end of prompt in minibuffer. 
  3. Then evaluate /dev/user.clj namespace and execute :dbg in REPL. 
  4.  You should now see the FlowStorm main window pop up. 

Basic Instrumentation 

With the project and REPL set up and FlowStorm started, we can try its basic features: instrumenting code and recording it. 

  1. On the left side of main window, you’ll find several tools. Click on “Browser” (see Figure 1). 

The Browser tool lists all loaded namespaces in your application.
If you correctly evaluated your user.clj file which requires quil, you should see several quil namespaces and your user namespace. 

You can select any namespace to see its functions, their argument lists, and potentially their docstrings. For this tutorial, we will instrument our user namespace. 

  1. Right-click on the user namespace in the Browser list.
    From the menu, select “Add instr prefix for user.*”.
    This tells FlowStorm to instrument all functions in the user namespace

Note: FlowStorm also lets you say which namespaces to instrument with a JVM property.

Record your first expression 

  1. After instrumenting the user namespace, switch from the “Browser” tool back to the “Flows” tool on the left side of the FlowStorm window. 
  2. In the “Flows” tool interface, you will see a record button (see Figure 2). Click the “Record” button to start capturing instrumented function calls. 

(If you don’t know what some FlowStorm button does, you can usually hover over it and get a tooltip.)

  1. With recording active, let’s feed FlowStorm its first bite of code. We’ll start with something incredibly simple.
    Return to your REPL and run a simple Clojure expression, for example: 

4. When this runs, an entry should show up in the “Flows” window. This is the trace of the + function call. 

Navigating the Recording

  1. You will see the recording of (+ 1 2 3) in the “Flows” window. Above the code form, there are buttons for navigation. Click on the button with a simple right arrow (1 in Figure 3) to step into the details of the form’s execution. 
  2. Now, direct your attention to the ‘Data Window’ on the right. This is your interactive lens into the runtime values of your code (2 in Figure 3). You can see this data in different ways; use the dropdown menu in the “Data Window” (it says :int by default). 

Working with Multiple Traces in a Single Flow 

FlowStorm records traces one after another in the current “flow” (one recording session). 

  1. (OPTIONAL) Click the “Pause” button (which replaces the “Record” button when recording) to stop the active recording. 
  2. (OPTIONAL) Click the “Record” button again to continue recording in the same flow. 
  3. Return to your REPL and evaluate another expression, for instance: 

4. You will see that the number of recorded steps in the current flow increases. You can navigate this new trace like the first one.

Organizing Work with Multiple Flows 

As your debugging sessions get more involved, you might be investigating different features or bugs simultaneously. Instead of one massive, confusing trace, FlowStorm lets you organize your work into multiple ‘Flows’.

If you want to have a new recording in a different “Flow”, you can pick another one (see Figure 4) and switch between them. They help organize different debug sessions. 

  1. Let’s select another flow, e.g., flow-1 
  2. Click Record (if it is not already on). 
  3. In your REPL, evaluate a simple expression like (reduce + (range 100)) 
  4. Observe this new trace in flow-1 (see Figure 5), you can switch flows anytime. 

Dealing with Exceptions

Let’s face it, exceptions happen, and Flowstorm has a good way to navigate them.

  1. Go back to your REPL and run: 

2. An exception drop-down will appear in “Flow” (see Figure 6). You can easily go to where the exception happened. 

Diving Into a More Complex Example with Quil 

Simple math is useful for a first look, but FlowStorm shines when dealing with more complex code. Let’s use a Quil example to explore further. 

  1. Open your user.clj file in your editor and add the following Clojure code. It defines a simple Quil sketch. 
(defn setup []
  (q/frame-rate 1)      ;; Set framerate to 1 FPS
  (q/background 200))   ;; Set the background colour


(defn make-ellipse [x y diam]
  (q/ellipse x y diam))


(defn draw []
  (q/stroke (q/random 255))             ;; Set the stroke colour to a random grey
  (q/stroke-weight (q/random 10))       ;; Set the stroke thickness randomly
  (q/fill (q/random 255))               ;; Set the fill colour to a random grey


  (let [diam (q/random 100)             ;; Set the diameter to a value between 0 and 100
        x    (q/random (q/width))       ;; Set the x coord randomly within the sketch
        y    (q/random (q/height))]     ;; Set the y coord randomly within the sketch
    (make-ellipse x y diam diam)        ;; Draw a circle at x y with the correct diameter
    ))


(q/defsketch fs-example            ;; Define a new sketch named example
  :title "Oh so many grey circles" ;; Set the title
  :settings #(q/smooth 2)          ;; Turn on anti-aliasing
  :setup setup                     ;; Specify the setup fn
  :draw draw                       ;; Specify the draw fn
  :size [323 200])                 ;; Set the sketch size

Before re-evaluating the namespace and running the Quil sketch: 

  1. In FlowStorm, click the “Clean all flows” button (on the left of the recording button) to clear all existing recordings. 
  2. Make sure the recording is on by clicking the “Record” button. 
  3. Now reload the user namespace. 

When the user namespace is evaluated and the sketch starts, a window titled “Oh so many grey circles” should appear, drawing circles. FlowStorm will begin to capture traces related to the draw and setup functions, because they are in the instrumented user namespace. You might see about 34 steps recorded. 

Working with Threads 

If you step through the traces after the sketch starts, you might not see much about the animation. This is because the Quil animation (the draw loop) usually runs in its own thread. 

FlowStorm makes navigating between threads a breeze

  1. Click on the “Threads” dropdown in FlowStorm “Flow” window. 
  2. You should see a list of threads. Select the thread responsible for the animation (see Figure 7; it should be named “Animation Thread”). 
  3. Once the animation thread is selected, you will see the traces generated by the setup and draw function calls, accumulating as the animation runs (if recording is still active). 

You can stop it now. 

TIP: FlowStorm can show all threads in one window. To do that, start recording by clicking on “Start recording of the multi-thread timeline” next to the “Record” button and then select “Multi-thread timeline browser” under “More tools”. 

Defining Values 

Spotted an interesting piece of data in a trace? Why not pull it directly into your REPL

  1. Navigate through the traces in the animation thread until you find a call to a Quil function q/stroke. In the “Data Window” on the right, you’ll see the value of q/stroke is a Clojure map (clojure.lang.PersistentArrayMap). 
  2. Suppose you want to inspect or manipulate this specific map in your REPL. FlowStorm lets you define any displayed value as a variable in your user namespace (or any other). With the value selected in the “Data Window”, click the “def” button (see Figure 8). 
  3. You’ll be prompted to give the new var a name. Enter a name (e.g., my-stroke-data) 

This value is now available in your user namespace in the REPL under the name you provided, allowing you to interact with it directly. 

Advanced Navigation: Quick Jump & Power Step 

As your application runs, traces can grow quite long. Manually stepping through hundreds or thousands of evaluations isn’t efficient. Say you are looking at q/stroke call and want to see the q/stroke call for the next circle drawn, without manually stepping through everything. This is where “Power Step” is useful. The “Power Step” feature has several modes. Here, we want to jump to the next time a function is called at the same place in the code (like the q/stroke line in your draw function). 

  1. Make sure you are on a q/stroke call in the trace view. You can use the “Quick Jump” feature (see Figure 9) – type user/draw there and pick the function (with an amount of calls next to it). It will take you to the function. Then move two steps more and you should be on q/stroke. 
  2. Pick the same-coord option from the “Power Step” dropdown menu and click the “Power Step forward” button (highlighted in Figure 10). 
  3. FlowStorm will jump to the next time the code at that place (the q/stroke call in draw) runs. You can see in the “Data Window” how values (like :current-stroke in the map) changed for the new circle. 

Custom Data Visualization 

FlowStorm’s default data view is useful, but sometimes a custom view gives better insight, especially for domain-specific data. Let’s make one for our Quil stroke.

Looking at the q/stroke data (a map with color info), it’s just numbers. It would be more intuitive if we could see the actual color represented visually. FlowStorm allows custom visualizers for this purpose. 

  1. Add the following code to your user.clj namespace. It registers a new visualizer for data that represents a Quil stroke. It uses JavaFX (which FlowStorm’s UI is built with) to draw a line with the stroke’s color. 
(require '[flow-storm.api :as fsa])
(require '[flow-storm.debugger.ui.data-windows.visualizers :as viz])
(require '[flow-storm.runtime.values :as fs-values])
(import '[javafx.scene.shape Line]
        '[javafx.scene.paint Color])


(viz/register-visualizer
 {:id :stroke ;; name of your visualized
  :pred (fn [val] (and (map? val) (:quil/stroke val))) ;; predicate
  :on-create (fn [{:keys [quil/stroke]}]
               {:fx/node (let [gray (/ (first (:current-stroke stroke)) 255) ;; gray normalization
                               color (Color/gray gray)
                               line (doto (Line.) ;; draw line
                                      (.setStartX 0.0)
                                      (.setStartY 0.0)
                                      (.setEndX 30)
                                      (.setEndY 30)
				      (.setStrokeWidth 5.0)
                                      (.setStroke color))]
                           line)})})


(fs-values/register-data-aspect-extractor ;; extract data from q/stroke 
 {:id :stroke
  :pred (fn [val _] (and (map? val) (:current-stroke val)))
  :extractor (fn [stroke _] {:quil/stroke stroke})})


(viz/add-default-visualizer (fn [val-data] (:stroke (:flow-storm.runtime.values/kinds val-data))) :stroke)
  1. If the Quil sketch window (“Oh so many grey circles”) is still open, close it. 
  2. In FlowStorm, clear any existing recordings. 
  3. Make sure the recording is on. 
  4. Re-evaluate your user namespace (which should restart the Quil sketch). 

Now, when you go to a trace involving q/stroke (or more precisely, the data map that contains the stroke information) in the animation thread, you should see a short line whose color matches the stroke color used for a circle (see Figure 11). 

If you use “Power Step” (set to same-coord) to jump to the next q/stroke data, you should see the color of the line in the custom visualizer change accordingly. 

Custom visualizers provide significant power. For even more advanced customization or integration with external tools, FlowStorm also supports a plugin system. Exploring plugins is beyond the scope of this initial tutorial, but it’s a feature to be aware of for future needs. 

Use of the Call Tree Tool 

Stepping line-by-line shows what happens, but how does it all fit together? The “Call Tree” tool shows a tree of function calls from a recording. This helps understand the structure of how the code runs and how functions call each other. The tree displays arguments or return values to help differentiate between multiple calls to the same function. 

  1. Keep the animation thread selected and open the “Call Tree” tool (see Figure 12). 
  2. The tool will display a tree. Look for repeated calls to user/draw or other functions from your sketch, like user/setup. 
  3. Click on one of the user/draw nodes in the call tree, and you should see a call to (user/make ellipse). Double-clicking any node takes you to it. 

Use of the Function List 

Need a quick summary of all functions that ran, or want to find every single call to user/draw? The ‘Function List’ tool gives you a flat, searchable index of all recorded function invocations.

  1. Open the “Function List” tool (see Figure 13). 
  2. You’ll see a list of functions. Find entries for functions from your code (e.g., user/draw) and library functions that were called (e.g., quil.core/ellipse, quil.core/random). 
  3. Pick a function from the list, for example, user/make-ellipse. A panel on the right should show all the individual calls of that function that were recorded. For each call, you can see its arguments and what it returned. 
  4. Like the “Call Tree” tool, double-clicking on a specific invocation will navigate you to it. 

Saving Positions: Bookmarks

Found a particularly interesting spot in a long trace that you know you’ll want to revisit? FlowStorm’s bookmarks save your place.

To save a bookmark: 

  1. Go to the step in the trace you want to bookmark. 
  2. Click on the bookmark button in the FlowStorm UI (see Figure 14). 
  3. It will ask for a name for your bookmark. Choose a descriptive name. 

Once saved, you can use the bookmark list in FlowStorm (“View” -> “Bookmarks”) to quickly jump back to that exact position in the trace at any time, without needing to manually step through again. You can also make bookmarks in your Clojure code. This lets you mark important points in your program’s run. Let’s add one to our Quil example. 

  1. First, make sure flow-storm.api namespace is available in your user.clj file. Let’s give it fsa alias. 
  2. Now change your setup function in user.clj to include a call to fsa/bookmark: 
(defn setup []
  (q/frame-rate 1)
  (q/background 200)
  (fsa/bookmark "Quil setup function complete")) ; <--- Add this line

3. Re-evaluate the namespace. Next time setup runs, a bookmark “Quil setup function complete” will be automatically created, and FlowStorm will jump to it. You can also find this new bookmark in FlowStorm’s bookmark list.

Sending Data to the Output Tool with tap> 

We all know println debugging. While sometimes useful, it can clutter your console and mix with other output. Clojure’s tap> offers a more structured way to inspect values, and FlowStorm can be its dedicated display. 

  1. Modify the draw function in your user.clj file to tap> the circle’s properties just before it’s drawn. 
(defn draw []
  (q/stroke (q/random 255))
  (q/stroke-weight (q/random 10))
  (q/fill (q/random 255))


  (let [diam (q/random 100)
        x    (q/random (q/width))
        y    (q/random (q/height))]
    (tap> {:event :circle-drawn, :x x, :y y, :diameter diam}) ; <--- Add this line
    (q/ellipse x y diam diam)))

2. Re-evaluate your user namespace and make sure FlowStorm is recording. 

3. In the Flowstorm UI, find and open the “Output” tool. 

4. As the Quil animation runs, you should see maps appearing in the “Output” tool. Each map is the data you tapped. This gives a log of data from your draw function (see Figure 15). 

Using the Printers Tool 

Sometimes, you want to keep an eye on a specific value or expression as it changes over many executions, without clicking through traces each time. The ‘Printers’ tool is like setting up a persistent watch window, tailored to exactly what you want to see.

  1. Make sure your Quil sketch is running and you have some traces recorded (e.g., in flow-0 and the “Animation thread” is selected). 
  2. In Flowstorm’s code stepping view, navigate to the traces of the draw function. Right-click on x in (q/ellipse …) and select “Add to prints” (see Figure 16). 
  3. Dialog will show up. You can set up the printer: 
  • Message format: Enter a descriptive string that will help you to identify the value, e.g., X of ellipse 
  • Expression: Used for transforming the value, let’s try int (that will change floats to integers). 
  1. Confirm the dialog. Now go to “Printers” that is hidden inside “More tools” dropdown in the “Flows” window (see figure 17). 
  2. A new “FlowStorm printers” window with definition of our existing printer will appear (see Figure 18). When you now click the refresh button (top left), you will see the value for every x transformed to int that you recorded. You can always redefine your printers and do this again. Double-clicking any printed value takes you to it

Search in Traces 

When your traces become vast landscapes of data, finding that one specific value or call can feel like searching for a needle in a haystack. FlowStorm’s search functionality can help you.

  1. Select the “Search” tool (from “More tools”, see Figure 17). A new “FlowStorm search” window should pop up. 
  2. Search for all the occurrences of the frame-rate. Type frame-rate into the search bar and execute the search. You should see all the occurrences of this key (see Figure 19). 
  3. Double-clicking any result takes you to it. 
  4. Feel free to experiment with the remaining search functions like “By predicate” etc. 
  1. Conclusion 

This tutorial showed you FlowStorm’s main features: setting up, first recordings, and using tools like custom views and printers. You saw how FlowStorm can help understand your Clojure code’s execution, making debugging easier

(Here is a gist with a complete user.clj example.)

The best way to learn is to use FlowStorm in your projects. The more you use it, the more you’ll see how it helps with complex code. The next section gives a quick look at other features. For more details, the official FlowStorm documentation is very helpful. 

  1. Some Additional FlowStorm Features Worth Mentioning 

This tutorial covered many things, but FlowStorm has more. Here’s a quick list of other features. Look at the official documentation for details: 

  • Metadata Navigation: Lets you see metadata of Clojure data structures in traces. 
  • datafy / nav Support: Works with Clojure’s datafy and nav to show custom data types better. 
  • EQL Filtering for Data Structures: Use EQL queries to find specific parts of big or nested data. 
  • Thread Breakpoints: Pause threads at specific functions, can be conditional on arguments (check platform for support ). 
  • Plugins: Add custom tools or use existing ones like Web Plugin, FlowBook Plugin, CLJS compiler plugin, or Async Flow Plugin.
    • Web Plugin – an experimental plugin for visualizing web application flows. 
    • FlowBook Plugin – allows you to store and replay your flows with an optional “flowbook”, which is a notebook that supports links to your recordings that can be used to explain your flows. 
    • Flow Storm CLJS compiler plugin – helps you visualize and move around your ClojureScript compilation recordings by representing them as an interactive graph. 
  • Flow Storm Async Flow Plugin – allows you to visualize your core.async.flow graph recorded activity from a graph 
  • Specific Loop Navigation: Right-click values in loops to jump to other iterations or start/end of loops. 
  • Output Tool – Capturing stdout and stderr: Set up “Output” tool to see out and err streams (Clojure only for streams ). 
  • Remote Debugging: Debug Clojure apps running on other machines, often with SSH. 
  • Dealing with Too Many Traces (Recording Limits): Set limits for recording functions or threads to control trace size and speed, helps avoid OutOfMemoryError. 
  • Handling Mutable Values: Use flow-storm.runtime.values/SnapshotP protocol to tell FlowStorm how to save the state of mutable values. 
  • Instrumentation Limitations & Control: Know about limits (e.g., very big forms, some recur, instrumenting clojure.core ) and use controls to pick what to instrument. 
  • General Programmable Debugging/Analysis: Use FlowStorm’s APIs (like in flow-storm.runtime.indexes.api ) to look at recorded traces with code from your REPL. There’s an in-REPL tutorial for this. 
  • Multi-thread timeline: Records and shows operations from many threads in order. Good for finding concurrency bugs. 
  • Editor Integration: Works with editors like Emacs (CIDER), VSCode (Calva), and IntelliJ (Cursive) to jump from FlowStorm to your code. 
  • Styling and Theming: Change UI look with themes (light/dark) or custom CSS.