Liking cljdoc? Tell your friends :D

Handling start failures

(ns darkleaf.di.tutorial.l-handling-start-failures-test
  (:require
   [clojure.test :as t]
   [darkleaf.di.core :as di]
   [darkleaf.di.utils :refer [catch-some]]))

A real system can fail halfway through start: the database came up, the queue worker came up, then the third component threw. If nothing stops what was already built, you leak resources — and some are non-shareable. A Jetty server holds a port. If you cannot stop it, you cannot start another one on the same port. The reference to the root is usually lost too, so the only escape is to restart the REPL.

DI handles this for you. When a build fails, it stops what was already built, and only then propagates the error.

Built components are stopped before the error escapes

dep builds successfully and returns the stopped atom. Its ::di/stop flips the atom to true. root depends on dep and then throws.

(defn dep
  {::di/stop (fn [stopped] (reset! stopped true))}
  [{stopped ::stopped}]
  stopped)

(defn root
  {::di/kind :component}
  [{_ `dep}]
  (throw (ex-info "build failed" {})))

catch-some captures whatever di/start throws. The original failure is wrapped as ::di/build-failure. ex-cause gives you the original. :stack in ex-data shows the chain of keys DI was building when the failure happened.

(t/deftest built-deps-are-stopped-test
  (let [*stopped (atom false)
        ex       (catch-some (di/start `root {::stopped *stopped}))]
    (t/is (= ::di/build-failure (-> ex ex-data :type)))
    (t/is (= [`root] (-> ex ex-data :stack)))
    (t/is (= "build failed" (-> ex ex-cause ex-message)))
    ;; dep was stopped before the error propagated
    (t/is @*stopped)))

When a stop itself fails

When a build fails, DI stops the components it already built. These stop functions can also throw. So there can be several errors at once: the original one and the stop errors. DI packs them all into the one exception it throws:

  • the thrown exception is the build failure;
  • its cause is the original error, see ex-cause;
  • the stop errors are attached to it as suppressed exceptions, see Throwable/.getSuppressed.
(defn dep-stop-throws
  {::di/stop (fn [_] (throw (ex-info "stop failed" {})))}
  []
  :built)

(defn root-after-bad-stop
  {::di/kind :component}
  [{_ `dep-stop-throws}]
  (throw (ex-info "build failed" {})))

(t/deftest stop-errors-are-suppressed-test
  (let [ex (catch-some (di/start `root-after-bad-stop))]
    (t/is (= ::di/build-failure (-> ex ex-data :type)))
    (t/is (= "build failed" (-> ex ex-cause ex-message)))
    (t/is (= ["stop failed"] (->> ex .getSuppressed (map ex-message))))))

That is the end of the tutorial. The Example app shows the pieces working together in a real project.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close