Skip to content
/ ex Public

In which we deal with exceptions the clojure way

License

Notifications You must be signed in to change notification settings

exoscale/ex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ex

Exception handling library for clojure

Api docs

  • cljdoc badge ex

  • cljdoc badge ex-manifold

  • cljdoc badge ex-auspex (completable future)

  • cljdoc badge ex-http

Installation

Clojars Project

Clojars Project

Clojars Project

Clojars Project

Rationale

  • We want to be able to express/handle exceptions via ex-info in a more formalised way.

  • We want to be able to support java like exceptions hierarchies, without having to define/extend classes.

  • We want the shape of our custom ex-infos to be consistent.

  • We want to have a default/generic categorization of ex-infos

  • We don't want to differ from the original try/catch/finally semantics.

  • We want to have the same mechanism for manifold.deferred/catch, CompletableFuture, optional via separate deps.

  • We want to minimize any performance penalty it might have.

  • We don't want to emit catch Throwable, stay as close as possible to what the user would write by hand.

How

This is an exception library, drop in replacement for try/catch/finally, that adds support for ex-info/ex-data with a custom clojure hierarchy that allows to express exceptions relations. It also comes with manifold support.

So we have exoscale.ex/try+, which supports vanilla catch/finally clauses.

If you specify a catch clause with a keyword as first argument things get interesting. We assume you always put an :exoscale.ex/type or :type key in the ex-infos you generate, and will match its value to the value of the key in the catch clause.

The basics

Essentially catch takes this form:

(catch :something m
   ;; where m is a binding to the ex-data (you can destructure at that level as well)
   )

So you can do things like that.

(require '[exoscale.ex :as ex])

(ex/try+

  (throw (ex-info "Argh" {::ex/type ::bar :foo "a foo"}))

  (catch ::foo data
    (prn :got-ex-data data))

  (catch ::bar {:as data :keys [foo]}
    ;; in that case it would hit this one
    (prn :got-ex-data-again foo))

  (catch ExceptionInfo e
   ;; this would match an ex-info that didn't get a hit with catch-ex-info)

  (catch Exception e (prn :boring))

  (finally (prn :boring-too)))

Exception hierarchies

We leverage a clojure hierarchy so you can essentially create exceptions relations/extending without having to mess with Java classes directly.

;; so bar is a foo

(ex/derive ::bar ::foo)

(ex/try+
  (throw (ex-info "I am a bar" {::ex/type ::bar})
  (catch ::foo d
    (prn "got a foo with data" d)
    (prn "Original exception instance is " (-> d meta ::ex/exception))))

Manifold support

We have exoscale.ex.manifold/catch that matches the semantics of a catch block in try+ but with a more manifold like feel.

(require '[exoscale.ex.manifold :as mx])
(require '[manifold.deferred :as d])

(-> (d/error-deferred (ex-info "boom" {::ex/type :bar}))
    (mx/catch :bar (fn [data] (prn "ex-data is: " data)))
    (d/catch (fn [ex] "... regular manifold handling here")))

How to get to the original exception

You can also get the full exception instance via the metadata on the ex-data we extract, it's under the :exoscale.ex/exception key. If you are within a try+ block you can also access it directly via &ex

Our default internal error type table

We suggest you also either use one of these as ::ex/type or derive your own with these.

Within the namespace :exoscale.ex:

category retry fix
:unavailable yes make sure callee healthy
:interrupted yes stop interrupting
:incorrect no fix caller bug
:forbidden no fix caller creds
:unsupported no fix caller verb
:not-found no fix caller noun
:conflict no coordinate with callee
:fault no fix callee bug
:busy yes backoff and retry

This is very much inspired by cognitect-labs/anomalies.

We have simple wrappers to create ex-info based on this: ex/ex-unavailable, ex/ex-interrupted and so on. Their signature is identical to ex-info otherwise. An ex-http/response->ex-info utility function that maps HTTP status codes onto this taxonomy is available in the ex.http namespace.

How to generate/use good ex-infos

  • Specify a :exoscale.ex/type key always

  • The type key should either be one of our base type or a descendent. Descendents should be avoided when possible.

  • If it's a rethrow or comes from another exception pass the original exception as cause (3rd arg of ex-info)

  • Have logging in mind when you create them. It's easier to pull predefined set of values from ELK or aggregate than searching on a string message.

  • ex-infos should contain enough info but not too much (don't dump a system/env map on its ex-data).

  • Preferably select the data you want to show in your ex-data instead of removing what you want to hide. If for some reason secrets end up in the upstream data source at least there's is no risk of leaking them with the exception that way.

  • If you use more than once the same type you might want to spec it

  • Avoid returning raw values in error-deferreds, return properly formated ex-infos (d/error-deferred ::foo) vs (d/error-deferred (ex-info ".." {...}))

  • Do not leak data that is meant to emulate a usage context (cloudstack error codes, http status codes). That should be handled by a middleware at the edge.

Helpers

We have a few helpers

  • exoscale.ex/ex-info.

    ;; shortest, just a msg and {::ex/type ::incorrect}
    (ex/ex-info "Oh no" ::incorrect)
    
    ;; same with some ex-data
    (ex/ex-info "Oh no" ::incorrect {:foo :bar})
    
    ;; same with cause
    (ex/ex-info "Oh no" ::my-error {:foo :bar} cause)
    
    ;; including derivation
    (ex/ex-info "Oh no" [::foo [::incorrect ::sentry-loggable]])
    
    (ex/ex-info "Oh no" [::foo [::incorrect ::sentry-loggable]] {...})
  • ex/ex-unavailable,ex/ex-interrupted, etc

    They are just shortcuts to ex-info with preset :type matching our base type table and built in validation.

    (throw (ex/ex-unavailable "Cannot reach foo" {:because :this}))
  • for testing you can use in exoscale.ex.test thrown-ex-info-type? similar to thrown? in clojure.test.

    (is (thrown-ex-info-type? ::foo (throw (ex-info "boom" {:type ::foo})))) -> true

    It dispatches via try+/catch, so relations are supported.

  • we support turning these kind of ex-infos into data via clojure.core.protocols/datafy and back into exceptions via exoscale.ex/map->ex-info.

=> (clojure.core.protocols/datafy (ex/ex-incorrect "boom" {:a 1} (ex/ex-incorrect "the-cause")))
#:exoscale.ex{:type :exoscale.ex/incorrect
              :message "boom"
              :data {:a 1}
              :deriving #{:exoscale.ex/foo :exoscale.ex/bar}
              :cause #:exoscale.ex{:type :exoscale.ex/incorrect
                                   :message "the-cause"
                                   :data {...}
                                   :deriving #{:exoscale.ex/foo :exoscale.ex/bar}}

=> (type (ex/map->ex-info *1))
clojure.lang.ExceptionInfo

Usage examples

Some real life examples of usage for this:

  • Deny all display of user exceptions to the end-user by default via top level middleware and only let through the ones marked safe to show via a derived type.

  • Skip sentry logging for some kind of exceptions (or the inverse)

About

In which we deal with exceptions the clojure way

Resources

License

Stars

Watchers

Forks

Packages

No packages published