failjure

repository·master·Indexed 19 days ago

https://github.com/adambard/failjure

A utility library for Clojure and ClojureScript that provides an alternative to exception-based error handling by treating failures as values. It includes the HasFailed protocol, short-circuiting threading macros like ok-> and as-ok->, and composition helpers such as attempt-all and try-all to facilitate functional purity and easier error propagation.

Tokens
1.6K
Snippets
9
Records
11
Agent score
15%

What's inside failjure

  1. How the `HasFailed` protocol works

    master

    HasFailed is the core protocol that describes a failed result. Failjure implements this protocol for Object (representing a successful value), Exception, and the built-in Failure record. This allows functions like failed? and message to work polymorphically across different types of failures.

    You can implement HasFailed for your own custom error types:

    (defrecord AnnotatedFailure [message data]
      f/HasFailed
      (failed? [self] true)
      (message [self] (:message self)))
    (defrecord AnnotatedFailure [message data]
      f/HasFailed
      (failed? [self] true)
      (message [self] (:message self)))
  2. Check for failure and get message with `failed?` and `message`

    master

    These functions are part of the HasFailed protocol. They allow you to check if a value is a failure (including Failure records, Java Exceptions, or JavaScript Errors) and retrieve the error message.

    (f/failed? some-value) ; => true if it's a failure
    (f/message some-failure) ; => "The error message"
  3. Handle failures with `attempt`

    master

    attempt accepts a value and a function. If the value is a failure, the function is called with that failure and the result is returned. If the value is successful, the value itself is returned.

    (defn handle-error [e] (str "Error: " (f/message e)))
    (f/attempt handle-error "Ok")  ;=> "Ok"
    (f/attempt handle-error (f/fail "failure"))  ;=> "Error: failure"
    (defn handle-error [e] (str "Error: " (f/message e)))
    (f/attempt handle-error "Ok")  ;=> "Ok"
    (f/attempt handle-error (f/fail "failure"))  ;=> "Error: failure"
  4. Thread failure-aware compositions with `ok->` and `ok->>`

    master

    ok-> and ok->> are threading macros similar to -> and ->>, but they short-circuit if any step in the chain returns a failure. This is useful for composing a series of functions where each step might fail.

    (defn validate-non-blank [data field]
      (if (empty? (get data field))
        (f/fail "Value required for %s" field)
        data))
    
    (let [result (f/ok->
                  data
                  (validate-non-blank :username)
                  (validate-non-blank :password)
                  (save-data))]
      (when (f/failed? result)
        (log (f/message result))
        (handle-failure result)))
    (let [result (f/ok->
                  data
                  (validate-non-blank :username)
                  (validate-non-blank :password)
                  (save-data))]
      (when (f/failed? result)
        (log (f/message result))
        (handle-failure result)))
  5. Adapt predicates to failures with `assert-with`

    master

    assert-with allows you to turn a predicate check into a failjure-compatible result. If the predicate passes, it returns the value; otherwise, it returns a Failure with the provided message.

    (f/assert-with some? "val" "is nil") ; => "val"
    (f/assert-with some? nil "is nil")     ; => #Failure{:message "is nil"}

    You can create custom assertion helpers using partial:

    (def assert-my-pred? (partial f/assert-with my-pred?))

    Pre-packaged helpers include assert-some?, assert-nil?, assert-not-nil?, assert-not-empty?, and assert-number?.

    (f/attempt-all
      [x (f/assert-with some? (some-fn) "some-fn failed!")
       y (f/assert-with integer? (some-integer-returning-fn) "Not an integer.")]
      (handle-success x)
      (f/when-failed [e] (handle-failure e)))
    (f/assert-with some? "val" "is nil") ; => "val"
    (f/assert-with some? nil "is nil")     ; => #Failure{:message "is nil"}
  6. Branch on success or failure with `if-let-` and `when-let-` helpers

    master

    Failjure provides several helpers for conditional branching based on whether a value is a success or a failure:

    • if-let-ok? / when-let-ok?: Executes the success branch if the value is not a failure.
    • if-let-failed? / when-let-failed?: Executes the failure branch if the value is a failure.

    Return values:

    • if- variants: If no else is provided, they return the value of x.
    • when- variants: They always return the value of x.
    (f/if-let-failed? [x (something-which-may-fail)]
      (handle-failure x)
      (handle-success x))
    (f/if-let-failed? [x (something-which-may-fail)]
      (handle-failure x)
      (handle-success x))
  7. Create a failure with `fail`

    master

    Use fail to create a Failure object. It accepts an error message and optional formatting arguments, using Clojure's standard format logic.

    (f/fail "Message here") ; => #Failure{:message "Message here"}
    (f/fail "Hello, %s" "Failjure") ; => #Failure{:message "Hello, Failjure"}
    (f/fail "Message here") ; => #Failure{:message "Message here"}
    (f/fail "Hello, %s" "Failjure") ; => #Failure{:message "Hello, Failjure"}
  8. Compose multiple failure-returning calls with `attempt-all`

    master

    attempt-all allows you to bind multiple values from functions that might return failures. It short-circuits on the first error encountered and returns that failure. You can use when-failed within the block to handle errors.

    ;; Basic usage
    (f/attempt-all [x "Ok" y (f/fail "Fail")] x) ; => #Failure{:message "Fail"}
    
    ;; With error handling
    (f/attempt-all [x "Ok" y (f/fail "Fail")]
      x
      (f/when-failed [e]
        (f/message e))) ; => "Fail"
    (f/attempt-all [x "Ok"
                    y (f/fail "Fail")]
      x
      (f/when-failed [e]
        (f/message e))) ; => "Fail"
  9. Handle multiple potentially throwing calls with `try-all`

    master

    try-all is a variant of attempt-all that automatically wraps each binding on the right side in a try*. This means if any of the expressions throw an exception, the exception is caught and returned as a failure, allowing the whole block to short-circuit gracefully.

    (try-all [x (/ 1 0)
              y (* 2 3)]
      y) ; => java.lang.ArithmeticException (returned, not thrown)
    (try-all [x (/ 1 0)
              y (* 2 3)]
      y) ; => java.lang.ArithmeticException (returned, not thrown)
  10. Use `as-ok->` for threading with an accumulator

    master

    as-ok-> works like Clojure's as-> macro, but it short-circuits the entire thread if any step returns a failure. This allows you to use a threading symbol (e.g., $) to pass a value through a sequence of operations that might fail.

    (f/as-ok-> "k" $
      (str $ "!")
      (str "O" $)) ; => "Ok!"
    
    (f/as-ok-> "k" $
      (str $ "!")
      (f/try* (Integer/parseInt $))
      (str "O" $)) ; => Returns (does not throw) a NumberFormatException
    (f/as-ok-> "k" $
      (str $ "!")
      (str "O" $)) ; => "Ok!"