Skip to content
This repository has been archived by the owner on Sep 11, 2020. It is now read-only.

fmnoise/anomalies-tools

Repository files navigation

anomalies-tools Build Status

Utility functions and macros for https://github.com/cognitect-labs/anomalies

Library is currenty in alpha, so anything may be changed in future versions.

Status

This library is not under active development and author recommends to consider https://github.com/dawcs/flow if you're looking for errors handling solution.

Usage

Leiningen dependency information:

[dawcs/anomalies-tools "0.1.3"]

Maven dependency information:

<dependency>
  <groupId>dawcs</groupId>
  <artifactId>anomalies-tools</artifactId>
  <version>0.1.3</version>
</dependency>

Let's write a function which will return anomaly in case of deref timeout:

(require '[anomalies-tools.core :as at :refer [!!]])
(require '[cognitect.anomalies :as a])

(defn get-value-with-fake-connection
  [value connection-timeout deref-timeout]
  (-> (Thread/sleep connection-timeout)
      (future value)
      (deref deref-timeout {::a/category ::a/busy
                            ::a/message "Connection timeout"})))

(get-value-with-fake-connection "hello" 1 100) ;; => "hello"
(get-value-with-fake-connection "hello" 100 1)
;; => #:cognitect.anomalies{:category :cognitect.anomalies/busy, :message "Connection timeout"}

Plain map syntax may look too verbose for someone, so there's handy construction helper:

(at/anomaly ::a/busy "Connection timeout")
;; => #:cognitect.anomalies{:category :cognitect.anomalies/busy, :message "Connection timeout"}

And a short form alias for true 1-liner lovers:

(!! ::a/busy "Connection timeout")
;; => #:cognitect.anomalies{:category :cognitect.anomalies/busy, :message "Connection timeout"}

There are also constructors for each category:

(at/busy "Connection timeout")
;; => #:cognitect.anomalies{:category :cognitect.anomalies/busy, :message "Connection timeout"}
(at/not-found)
;; => #:cognitect.anomalies{:category :cognitect.anomalies/not-found}
(at/forbidden {:user-id 123})
;; => #:cognitect.anomalies{:category :cognitect.anomalies/forbidden :data {:user-id 123}}

Construction helper functions are very flexible regarding arguments:

;; no need for message?
(at/busy)
;; => #:cognitect.anomalies{:category :cognitect.anomalies/busy}

;; need to store some additional data along with message?
(!! ::a/forbidden "Cannot perform operation" {:user-id 2128506})
;; => #:cognitect.anomalies{:category :cognitect.anomalies/forbidden, :message "Cannot perform operation", :data {:user-id 2128506}}

;; just data is enough?
(!! ::a/forbidden {:user-id 2128506})
;; => #:cognitect.anomalies{:category :cognitect.anomalies/forbidden, :data {:user-id 2128506}}

Default category is :cognitect.anomalies/fault (later we'll see how to change that)

;; the smallest possible anomaly constructor
(!!)
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault}

(!! "Cannot perform operation" {:user-id 2128506})
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault, :message "Cannot perform operation", :data {:user-id 2128506}}

(!! {:user-id 2128506})
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault, :data {:user-id 2128506}}

If we want other default category, wrapping code into with-default-category macro does the trick:

(at/with-default-category
 ::a/conflict
 (!! "Something went wrong"))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/conflict, :message "Something went wrong"}

So, how we can handle anomalies? First, we can check if value is anomaly with anomaly? function:

(at/anomaly? (!!)) ;; => true
(at/anomaly? {:user 1}) ;; => false
(at/anomaly? (Exception. "Bad stuff")) ;; => false

This can be useful for imperative style error checking:

(let [result (do-stuff)]
  (if (at/anomaly? result)
    (say-oooops result)
    (say-hooray result)))

How about functional programming?

(inc 1) ;; => 2
(inc (!!)) ;; BOOOM!!! Unhandled java.lang.ClassCastException clojure.lang.PersistentArrayMap cannot be cast to java.lang.Number

How to make function aware of anomalies? aware and rescue to the help! aware makes function wrapper which will call given function with non-anomaly argument and return given argment otherwise:

(def ainc (at/aware inc))
(ainc 1) ;; => 2
(ainc (!!)) ;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault}

rescue is dedicated to produce value from given anomaly returning given argument otherwise:

;; turning anomaly into http response
(def category->status
  {::a/forbidden 403
   ::a/not-found 404
   ::a/conflict  409
   ::a/busy      429
   ::a/fault     500})

(defn handle [anom]
  {:status (-> anom ::a/category category->status)
   :body (::a/message anom)})

(at/rescue handle 1) ;; => 1
(at/rescue handle (!! "Something went wrong")) ;; => {:status 500 :body "Something went wrong"}

Both aware and rescue accept function as first argument which makes it perfect for ->> macro

(->> 1 (at/aware inc) (at/aware str))
;; => "2"

(->> (!!) (at/aware inc) (at/aware str))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault}

(->> (!! "Oops")
     (at/aware inc)
     (at/aware str)
     (at/rescue (comp clojure.string/upper-case ::a/message)))
;; => "OOPS"

Do you like some-> and some->> power for dealing with nil values? There's analogs for anomalies:

(at/aware-> 1 inc) ;; => 2
(at/aware-> (!!) inc) ;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault}
(at/aware-> 1 (!!) inc) ;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault, :data 1}

(at/aware->> [1 2 3] (map inc)) ;; => (2 3 4)
(at/aware->> (!!) (map inc))   ;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault}
(at/aware->> [1 2 3] (!! ::a/conflict "Ooops") (map inc))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/conflict, :message "Ooops", :data [1 2 3]}

When functional chains are required, but macros magic is not desired, chain function may fit the needs:

(at/chain [1 2 3] (partial map inc)) ;; => (2 3 4)
(at/chain [1 2 3] (partial at/unsupported) (partial map inc))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/unsupported, :data [1 2 3]}

And of course there's opposite case, when we need to handle caught anomaly. caught does the job and makes sure that anomaly is returned from chain:

(at/caught
  (at/forbidden "Bad password" {:user-id 2128506})
  (comp prn ::a/message) ;; prn returns nil, so initial anomaly is passed to next function in chain
  (comp prn ::a/category))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/forbidden, :message "Bad password", :data {:user-id 2128506}}

For non-anomaly value, chain in completely skipped and given value is returned immediately:

(at/caught 1 (comp prn ::a/message)) ;; => 1

If some function in chain returns another anomaly, it's passed to next function in chain:

(at/caught
  (at/conflict "Uh-oh")
  (fn [x] (at/busy x)) ;; producing new anomaly from given one
  (comp prn at/category)) ;; prints :busy
;; => #:cognitect.anomalies{:category :cognitect.anomalies/busy, :data #:cognitect.anomalies{:category :cognitect.anomalies/conflict, :message "Uh-oh"}}

caught and chain accept value as first argument so can be used together in -> macro:

(-> "hello"
     at/anomaly
     (at/chain clojure.string/upper-case)
     (at/caught (comp prn at/message))) ;; prints anomaly message to console
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault, :message "hello"}

Returning to imperative error handling example, we can rewrite it using functional chain:

(-> (do-stuff)
    (at/chain say-hooray)
    (at/caught say-oooops))

Often we need to fallback to some default value. either can help in this case:

(at/either (!!) 1) ;; => 1
(apply at/either [(at/busy) (at/fault) (at/conflict) (at/not-found) 1]) ;; => 1

If only anomaly values are given to either, then last given value is returned:

(at/either (at/busy) (at/unsupported))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/unsupported}

So either is good companion for chain and caught in -> macro:

(-> "hello"
    at/anomaly
    (at/chain clojure.string/upper-case)
    (at/caught prn)
    (at/either "goodbye"))
;; => "goodbye"

By supporting multiple args, either can be also used on its own similarly to or:

(defn load-from-db [id]
  (if (= id 1)
    {:role "user"}
    (at/not-found)))

(defn load-from-cache [id]
  (if (= id 2)
    {:role "admin"}
    (at/not-found)))

(def default-settings {:role "guest"})

(defn user-settings [id]
  (at/either
    (load-from-cache id)
    (load-from-db id)
    default-settings))

(user-settings 1) ;; => {:role "user"}
(user-settings 2) ;; => {:role "admin"}
(user-settings 3) ;; => {:role "guest"}

alet is anomalies aware version of let macro:

(at/alet [a 1 b 2] (+ a b)) ;; => 3
(at/alet [a 1 b (!!)] (+ a b))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault}

alet calculates bindings until anomaly is returned. In the following example exception is not thrown:

(at/alet [a 1
          b (!!)
          c (throw (Exception.))]
  (+ a b))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault}

Sometimes we need to deal with Java exceptions. We can turn any caught exception into anomaly with catch-all macro:

(at/catch-all (/ 1 0))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault, :message "Divide by zero", :data #error {...}

If we need to throw some exceptions and catch all others, catch-except fits for that purpose:

(at/catch-except #{NullPointerException} (/ 1 0))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault, :message "Divide by zero", :data #error {...}
(at/catch-except #{NullPointerException} (/ 1 nil)) ;; throws java.lang.NullPointerException

Need to catch only certain exceptions and throw all others? catch-only does the job:

(at/catch-only #{NullPointerException} (/ 1 0)) ;; throws java.lang.ArithmeticException
(at/catch-only #{NullPointerException} (/ 1 nil))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault, :data #error {...}

WARNING! at the moment, exceptions hierarchy doesn't affect processing, e.g. specifying Throwable will catch only Throwable but none of it's descendants. Currently that reflects library author's vision, but things may change in future.

By default caught anomalies will be filled with default :category, :message extracted from exception and :data containing caught exception instance. We may want another behavior so catch-anomaly macro gives us full control for all that options along with list of exceptions to catch or throw:

(at/catch-anomaly
 {:category ::a/conflict
  :message "Uh-oh"
  :data (atom 1)}
 (/ 1 0))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/conflict, :message "Uh-oh", :data #atom[1 0x1ef83f75]}

(at/catch-anomaly
 {:message "Uh-oh"
  :only #{ArithmeticException}}
 (/ 1 0))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/fault, :message "Uh-oh", :data #error {...}

(at/catch-anomaly
 {:except #{ArithmeticException}}
 (/ 1 0)) ;; throws java.lang.ArithmeticException

(at/with-default-category
  ::a/conflict
  (at/catch-all (/ 1 0)))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/conflict, :message "Divide by zero", :data #error {...}

And finally we may need to assign some category for certain class of exceptions. That's also possible with with-exception-categories macro:

(at/with-exception-categories
  {NullPointerException ::a/unsupported}
  (at/catch-all (+ 1 nil)))
;; => #:cognitect.anomalies{:category :cognitect.anomalies/unsupported, :data #error {...}

TODO

  • ClojureScript support

License

Copyright © 2017 Cognitect, Inc. All rights reserved. Copyright © 2018 DAWCS

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

About

Anomalies handling tools

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published