├── .travis.yml ├── .gitignore ├── project.clj ├── src └── try_let.clj ├── README.md └── test └── t_try_let.clj /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: lein midje 3 | jdk: 4 | - oraclejdk9 5 | - oraclejdk8 6 | - openjdk8 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject try-let "1.3.1-SNAPSHOT" 2 | :description "Better exception handling for Clojure let expressions" 3 | :url "https://github.com/rufoa/try-let" 4 | :license 5 | {:name "Eclipse Public License" 6 | :url "http://www.eclipse.org/legal/epl-v10.html"} 7 | :dependencies 8 | [[org.clojure/clojure "1.10.0"] 9 | [org.clojure/core.specs.alpha "0.2.44"]] 10 | :profiles 11 | {:dev 12 | {:dependencies 13 | [[midje "1.9.6"] 14 | [slingshot "0.12.2"]] 15 | :plugins 16 | [[lein-midje "3.2"]]}} 17 | :scm 18 | {:name "git" 19 | :url "https://github.com/rufoa/try-let"} 20 | :main try-let) -------------------------------------------------------------------------------- /src/try_let.clj: -------------------------------------------------------------------------------- 1 | (ns try-let 2 | (:require 3 | [clojure.spec.alpha :as s] 4 | [clojure.core.specs.alpha :as cs])) 5 | 6 | 7 | (s/def ::catch 8 | (s/and 9 | list? 10 | #(>= (count %) 3) 11 | #(= (first %) 'catch))) 12 | 13 | (s/def ::else 14 | (s/and 15 | list? 16 | #(= (first %) 'else))) 17 | 18 | (s/def ::finally 19 | (s/and 20 | list? 21 | #(= (first %) 'finally))) 22 | 23 | (s/def ::then 24 | #(not (and 25 | (list? %) 26 | (#{'catch 'else 'finally} (first %))))) 27 | 28 | 29 | (s/def ::try-let-body 30 | (s/alt 31 | :stanzas (s/cat 32 | :thens (s/* ::then) 33 | :catches (s/* ::catch) 34 | :finally (s/? ::finally)) 35 | :stanzas (s/cat 36 | :catches (s/* ::catch) 37 | :finally (s/? ::finally) 38 | :thens (s/* ::then)))) 39 | 40 | (s/def ::try+-let-body 41 | (s/alt 42 | :stanzas (s/cat 43 | :thens (s/* ::then) 44 | :catches (s/* ::catch) 45 | :else (s/? ::else) 46 | :finally (s/? ::finally)) 47 | :stanzas (s/cat 48 | :catches (s/* ::catch) 49 | :else (s/? ::else) 50 | :finally (s/? ::finally) 51 | :thens (s/* ::then)))) 52 | 53 | 54 | (s/fdef try-let 55 | :args (s/cat 56 | :bindings ::cs/bindings 57 | :body ::try-let-body)) 58 | 59 | (s/fdef try+-let 60 | :args (s/cat 61 | :bindings ::cs/bindings 62 | :body ::try+-let-body)) 63 | 64 | 65 | (defmacro try-let 66 | [bindings & body] 67 | (let [[_ {:keys [thens catches finally]}] (s/conform ::try-let-body body) 68 | bindings-destructured (destructure bindings) 69 | bindings-ls (take-nth 2 bindings-destructured) 70 | gensyms (take (count bindings-ls) (repeatedly gensym))] 71 | `(let [[ok# ~@gensyms] 72 | (try 73 | (let [~@bindings-destructured] [true ~@bindings-ls]) 74 | ~@(map 75 | (fn [stanza] 76 | (let [[x y z & body] stanza] 77 | `(~x ~y ~z [false (do ~@body)]))) 78 | catches) 79 | ~@(when finally [finally]))] 80 | (if ok# 81 | (let [~@(interleave bindings-ls gensyms)] 82 | ~@thens) 83 | ~(first gensyms))))) 84 | 85 | (defmacro try+-let 86 | [bindings & body] 87 | (let [[_ {:keys [thens catches else finally]}] (s/conform ::try+-let-body body) 88 | bindings-destructured (destructure bindings) 89 | bindings-ls (take-nth 2 bindings-destructured) 90 | gensyms (take (count bindings-ls) (repeatedly gensym))] 91 | `(let [[ok# ~@gensyms] 92 | (slingshot.slingshot/try+ 93 | (let [~@bindings-destructured] [true ~@bindings-ls]) 94 | ~@(map 95 | (fn [stanza] 96 | (let [[x y z & body] stanza] 97 | `(~x ~y ~z [false (do ~@body)]))) 98 | catches) 99 | ~@(when else [else]) 100 | ~@(when finally [finally]))] 101 | (if ok# 102 | (let [~@(interleave bindings-ls gensyms)] 103 | ~@thens) 104 | ~(first gensyms))))) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | try-let 2 | ======= 3 | 4 | `try-let` is a Clojure macro designed to make handling some exceptions slightly nicer. It acts like `let`, but allows you to catch exceptions which may be thrown inside the binding vector. Exceptions thrown inside the body of the `try-let` are deliberately ignored. 5 | 6 | [![Build Status](https://travis-ci.org/rufoa/try-let.png?branch=master)](https://travis-ci.org/rufoa/try-let) 7 | 8 | ## Installation ## 9 | 10 | `try-let` is in Clojars. To use it in a Leiningen project, add it to your project.clj dependencies: 11 | 12 | [![Clojars Project](https://clojars.org/try-let/latest-version.svg)](https://clojars.org/try-let) 13 | 14 | then require `try-let` in your code: 15 | 16 | ```clojure 17 | (ns my.example 18 | (:require [try-let :refer [try-let]])) 19 | ``` 20 | 21 | ## Motivation ## 22 | 23 | It can be quite difficult to combine `try/catch` with `let` properly. Clojure pushes you towards one of two patterns, neither of which is ideal. 24 | 25 | ```clojure 26 | (try 27 | (let [value (func-that-throws)] 28 | (act-on-value value)) 29 | (catch Exception e 30 | (log/error e "func-that-throws failed"))) 31 | ``` 32 | 33 | In the above pattern, the scope of the `try/catch` is too great. In addition to `func-that-throws`, it also affects `act-on-value`. 34 | 35 | ```clojure 36 | (let [value 37 | (try (func-that-throws) 38 | (catch Exception e (log/error e "func-that-throws failed")))] 39 | (act-on-value value)) 40 | ``` 41 | 42 | In the above pattern, the scope of the `try/catch` is correct, affecting only `func-that-throws`, but when an exception is caught, `act-on-value` is evaluated regardless and must handle the exceptional case when `value` is nil. 43 | 44 | ## Use ## 45 | 46 | With `try-let`, we can instead do: 47 | 48 | ```clojure 49 | (try-let [value (func-that-throws)] 50 | (act-on-value value) 51 | (catch Exception e 52 | (log/error e "func-that-throws failed"))) 53 | ``` 54 | 55 | This allows the scope of the `try/catch` to be made as precise as possible, affecting only `func-that-throws`, and for evaluation to only proceed to `act-on-value` when `value` is obtained without error. In this way, `try-let` can be thought of as similar to `if-let`, where the body is only evaluated when the value of the binding vector is not nil. 56 | 57 | You can have multiple `catch` stanzas for different exceptions. Much of what you'd expect to work in a normal `let` works: 58 | 59 | ```clojure 60 | (try-let [val-1 (risky-func-1) 61 | val-2 (risky-func-2 val-1)] 62 | (log/info "using values" val-1 "and" val-2) 63 | (* val-1 val-2) 64 | (catch SpecificException _ 65 | (log/info "using our fallback value instead") 66 | 123) 67 | (catch RuntimeException e 68 | (log/error e "Some other error occurred") 69 | (throw e)) 70 | (finally 71 | (release-some-resource))) 72 | ``` 73 | 74 | As an alternative, you can also put `catch` stanzas before other body expressions: 75 | 76 | ```clojure 77 | (try-let [val-1 (risky-func-1)] 78 | (catch Exception e 79 | (log/error e "Problem calling risky-func-1") 80 | 0) 81 | (try-let [val-2 (risky-func-2 val-1)] 82 | (catch Exception e 83 | (log/error e "Problem calling risky-func-2") 84 | 0) 85 | (log/info "using values" val-1 "and" val-2) 86 | (* val-1 val-2))) 87 | ``` 88 | 89 | This makes the code logic more linear, where exceptions are handled closer to where they appear. 90 | 91 | ## Slingshot support ## 92 | 93 | There is also a `try+-let` macro which is compatible with [slingshot](https://github.com/scgilardi/slingshot)-style `catch` stanzas. 94 | 95 | ## License ## 96 | 97 | Copyright © 2015-2019 [rufoa](https://github.com/rufoa) 98 | 99 | Distributed under the Eclipse Public License, the same as Clojure. -------------------------------------------------------------------------------- /test/t_try_let.clj: -------------------------------------------------------------------------------- 1 | (ns t-try-let 2 | (:use midje.sweet) 3 | (:require 4 | [try-let :refer [try-let try+-let]] 5 | [slingshot.slingshot :refer [try+ throw+]])) 6 | 7 | (defn slingshot-exception 8 | [exception-map] 9 | (slingshot.support/get-throwable 10 | (slingshot.support/make-context 11 | exception-map 12 | (str "throw+: " map) 13 | nil 14 | (slingshot.support/stack-trace)))) 15 | 16 | (fact "simple let behaviour works" 17 | 18 | (try-let [] 19 | true) 20 | => true 21 | 22 | (try-let [x true] 23 | x) 24 | => true) 25 | 26 | (fact "exceptions are caught just inside binding vector" 27 | 28 | (try-let [x (/ 1 0)] x) 29 | => (throws ArithmeticException) 30 | 31 | (try-let [x (/ 1 0)] 32 | x 33 | (catch ArithmeticException _ 2)) 34 | => 2 35 | 36 | (try-let [x (/ 1 0)] 37 | x 38 | (catch ArrayIndexOutOfBoundsException _ 2)) 39 | => (throws ArithmeticException) 40 | 41 | (try-let [x (/ 1 0)] 42 | x 43 | (catch ArrayIndexOutOfBoundsException _ 3) 44 | (catch ArithmeticException _ 2)) 45 | => 2 46 | 47 | (try-let [] 48 | (/ 1 0) 49 | (catch ArithmeticException _ 2)) 50 | => (throws ArithmeticException)) 51 | 52 | (fact "sequential assignment works properly" 53 | 54 | (try-let [x true y x] 55 | [x y]) 56 | => [true true] 57 | 58 | (try-let [x true y x x false] 59 | [x y]) 60 | => [false true]) 61 | 62 | (fact "expressions are not evaluated multiple times" 63 | 64 | (let [eval-count (atom 0)] 65 | (try-let [x (do (swap! eval-count inc) eval-count)] 66 | @x)) 67 | => 1) 68 | 69 | (fact "implicit do works in body and catch stanzas" 70 | 71 | (let [evaled (atom false)] 72 | (try-let [] 73 | (swap! evaled not) 74 | @evaled)) 75 | => true 76 | 77 | (let [evaled (atom false)] 78 | (try-let [x (/ 1 0)] 79 | false 80 | (catch ArithmeticException _ 81 | (swap! evaled not) 82 | @evaled))) 83 | => true) 84 | 85 | (fact "works with slingshot" 86 | 87 | ; uncaught slingshot exceptions escape try+-let intact 88 | (try+ 89 | (try+-let [x (throw+ {:type :foo})] false) 90 | (catch [:type :foo] _ true)) 91 | => true 92 | 93 | ; pattern matching 94 | (try+-let [x (throw+ {:type :bar})] 95 | false 96 | (catch [:type :foo] _ 1) 97 | (catch [:type :bar] _ 2)) 98 | => 2 99 | 100 | ; catch-first syntax 101 | (try+-let [x (throw+ {:type :bar})] 102 | (catch [:type :foo] _ 1) 103 | (catch [:type :bar] _ 2) 104 | false) 105 | => 2 106 | 107 | ; slingshot &throw-context works inside catch 108 | (try+-let [x (throw+ {:foo :bar})] 109 | false 110 | (catch [] _ (:object &throw-context))) 111 | => {:foo :bar}) 112 | 113 | (fact "destructuring works" 114 | 115 | ; vectors 116 | (try-let [[a b] [1 2] 117 | [c & d] [3 4 5]] 118 | [a b c d]) 119 | => [1 2 3 [4 5]] 120 | 121 | ; maps 122 | (try-let [{:keys [a b]} {:a 1 :b 2} 123 | {:strs [c d]} {"c" 3 "d" 4} 124 | {{{:keys [e] :as f} :y} :x} {:x {:y {:e 5}}}] 125 | [a b c d e f]) 126 | => [1 2 3 4 5 {:e 5}]) 127 | 128 | (fact "finally stanza works" 129 | 130 | ; finally stanza evaluated when exception is not raised 131 | (let [evaled (atom false)] 132 | (try-let [] 133 | @evaled 134 | (finally (swap! evaled not)))) 135 | => true 136 | 137 | ; finally stanza evaluated when exception is raised and caught 138 | (let [evaled (atom false)] 139 | (try-let [x (/ 1 0)] 140 | (catch Exception _) 141 | (finally (swap! evaled not))) 142 | @evaled) 143 | => true 144 | 145 | ; finally stanza evaluated when exception is raised but not caught 146 | (let [evaled (atom false)] 147 | (try 148 | (try-let [x (/ 1 0)] 149 | (catch ArrayIndexOutOfBoundsException _) 150 | (finally (swap! evaled not))) 151 | (catch Exception _ @evaled))) ; ignore uncaught ArithmeticException 152 | => true) 153 | 154 | (fact "catch-first syntax works" 155 | 156 | (try-let [x (/ 1 0)] 157 | (catch ArithmeticException _ 2) 158 | x) 159 | => 2 160 | 161 | (try-let [x (/ 1 0)] 162 | (catch ArrayIndexOutOfBoundsException _ 2) 163 | x) 164 | => (throws ArithmeticException) 165 | 166 | (try-let [x (/ 1 0)] 167 | (catch ArrayIndexOutOfBoundsException _ 3) 168 | (catch ArithmeticException _ 2) 169 | x) 170 | => 2 171 | 172 | (try-let [] 173 | (catch ArithmeticException _ 2) 174 | (/ 1 0)) 175 | => (throws ArithmeticException) 176 | 177 | (let [evaled (atom false)] 178 | (try-let [] 179 | (catch Exception _) 180 | (finally (swap! evaled not)) 181 | @evaled)) 182 | => true) --------------------------------------------------------------------------------