A library that provides a with-open+
macro that addresses some shortcomings of Clojure's standard with-open
, offering:
- Destructuring Support: Bindings in
with-open+
fully support destructuring, just likelet
. - Custom Close Functions: You can specify a custom closing function for each resource via a protocol, interface, or metadata.
- Preserves Original Exception: If an exception occurs in the body,
with-open+
throws the original exception, not one from the closing process. - Suppressed Close Exceptions: Exceptions that occur during resource closing are attached as suppressed exceptions to the original body exception, providing comprehensive error information.
- Hints for Close Errors: Exceptions during closing include hints to help identify which resource failed to close, simplifying debugging.
;; deps.edn
{:deps {us.chouser/open {:git/url "https://github.com/chouser/open"
:git/tag "v1.0"
:git/sha "4866d47"}}}
The with-open+
macro works similarly to Clojure's built-in with-open
, but adds several important enhancements:
(require '[us.chouser.open :refer [with-open+ with-close-fn]])
;; Simple usage with Java closeables
(with-open+ [reader (java.io.StringReader. "hello world")]
(println (.read reader)))
;; Using destructuring
(with-open+ [{:keys [a b]} (get-resource-that-needs-closing)]
(do-something-with a b))
;; Multiple clauses are closed in reverse order
(with-open+ [a (io/reader "a.txt")
b (io/reader "b.txt")]
(read-from-both a b))
For non-Closeable objects, you can attach a custom close function using with-close-fn
:
;; Define a custom resource
(def my-map-resource
(with-close-fn {:type :map-resource, :value 10}
#(println "Closing map resource:" %)))
;; Use it in with-open+
(with-open+ [{resource-type :type, resource-value :value} my-map-resource]
(println "Resource type:" resource-type)
(println "Resource value:" resource-value))
;; Prints:
;; Resource type: :map-resource
;; Resource value: 10
;; Closing map resource: {:type :map-resource, :value 10}
For mutable reference objects like atom
, use add-close-fn!
instead. And of
course you can still define an object that implements java.lang.AutoCloseable
or
java.io.Closeable
interfaces or the us.chouser.open/Closeable
protocol:
(with-open+ [x (reify
Object (toString [_] "object x")
java.lang.AutoCloseable (close [_] (prn :close-x)))]
(prn :body (str x)))
;; Prints:
;; :body "object x"
;; :close-x
One of the key improvements in with-open+
is its handling of exceptions. When an exception occurs:
- The original exception is preserved as the primary exception
- Any exceptions during resource closing are attached as suppressed exceptions
- Diagnostic information is added to help identify which resource failed to close
(with-open+ [a (with-close-fn [:object-a] (fn [_] (prn :close-a)))
b (with-close-fn [:object-b] (fn [_] (throw (ex-info "close failed" {}))))]
(prn :body)
(throw (ex-info "body failed" {})))
;; Prints:
;; :body
;; :close-a
;; Throws:
;; Execution error (ExceptionInfo)...
;; body failed
As with Clojure's with-open
, event after the body throws an exception, b
is
closed. And even thought b
threw during closing, a
is closed anyway.
But with-open+
differs in that it suppresses the closing error and instead
throws the original body failed
exception.
You can use .getSuppressed
on the thrown exception to see any exceptions thrown during closing:
(.getSuppressed *e)
[#error {
:cause "close failed"
:data {}
:via [{:type clojure.lang.ExceptionInfo
:message "Error during closing"
:data {:hint b} ;; <-- the :hint indicates which clause threw
:at [us.chouser.open$close_all$fn__19366 invoke "open.clj" 52]}
{:type clojure.lang.ExceptionInfo
:message "close failed"
:data {}
:at [us.chouser.open_test$eval20087$fn__20090 invoke "NO_SOURCE_FILE" 122]}]}]
The Closeable
protocol is extended to:
java.io.Closeable
(calls.close
)java.lang.AutoCloseable
(calls.close
)clojure.lang.IMeta
(uses the:us.chouser.open/close
metadata function)nil
(no-op)
This library was inspired by James Henderson's jarohen/with-open and David McNeil's fork of it which adds the exception suppression among other things.
A key difference is that this library does not support the callback mechanism because that pattern has some drawbacks:
- Creates extra stack frames in traces, multiple for each clause in the bindings
- Can tempt developers to use it to obscure dynamic var binding
The metadata approach used here is less flexible because it requires that the closeable support metadata. If you prefer the callback approach, use one of those libraries. They've served me well.
Copyright © 2025 Chris Houser
Distributed under the Eclipse Public License version 1.0