5
votes

I found it surprisingly tricky to define a macro to do error handling in both clj and cljs. I assumed it was a simple matter of swapping Exception with js/Error, but it turned out to be more complicated than that.

At first, I tried this:

(defmacro my-macro
  [& forms]
 `(try
    ~@forms
    (catch #?(:clj Exception :cljs js/Error) e#
      ,,,)))

But this produced Exception every time. I soon realized that the problem was that the macro was being called during the compilation of my cljs files, which happens in a clj environment. Therefore, I would have to have the macro return a form that would resolve to the correct exception class at runtime. I tried this:

(def exception-class
  #?(:clj Exception :cljs js/Error))

(defmacro my-macro
  [& forms]
 `(try
    ~@forms
    (catch exception-class e#
      ,,,)))

Now it worked in cljs, but not in clj!!! After some experimentation, I discovered that JVM Clojure (apparently) does not allow you to refer to the exception class indirectly. You have to refer to Exception directly by name.

So finally, I settled on this:

(def fake-java
  #?(:cljs (clj->js {:lang {:Exception js/Error}})))

(defmacro my-macro
  [& forms]
 `(let [~'java fake-java]
    (try
      ~@forms
      (catch Exception e#
        ,,,))))

Exception expands to java.lang.Exception, which now resolves to the correct exception class at runtime in both clj and cljs.

My question is, is there a better way to this? And why does JVM Clojure not allow referring to the exception class indirectly, but ClojureScript does?

Update

With ClojureMostly's help, I have refactored the macro like this, and it works:

(defmacro my-macro
  [& forms]
 `(try
    ~@forms
    (catch ~(if (:ns &env) 'js/Error 'Exception) e#
      ,,,)))
2

2 Answers

3
votes

You could refactor your macro to be expressed in terms of a function call. This function would accept a "thunk" of the forms and wrap it in the try (rather than doing this directly in the macro).

For example:

(defmacro my-macro [& forms]
  `(my-macro* (fn []
                ~@forms)))

Then you can define my-macro* as:

(defn my-macro* [f]
  (try
    (f)
    (catch #?(:clj Exception :cljs js/Error) e
      ...)))
2
votes

The common way to do this is to check for the :ns key in the special &env binding that you have in a defmacro. Copied from plumatic/schema:

(defn cljs-env?
  "Take the &env from a macro, and tell whether we are expanding into cljs."
  [env]
  (boolean (:ns env)))

(defmacro try-catchall
  "A cross-platform variant of try-catch that catches all exceptions.
   Does not (yet) support finally, and does not need or want an exception class."
  [& body]
  (let [try-body (butlast body)
        [catch sym & catch-body :as catch-form] (last body)]
    (assert (= catch 'catch))
    (assert (symbol? sym))
    (if (cljs-env? &env)
      `(try ~@try-body (~'catch js/Object ~sym ~@catch-body))
      `(try ~@try-body (~'catch Throwable ~sym ~@catch-body)))))

Usage:

(macros/try-catchall (f)  (catch e# ::exception))