This document summarizes a talk about elegant error handling in Clojure. It discusses some problems with conventional error handling approaches that use exceptions, like code becoming difficult to maintain and lack of composability. It then introduces an alternative approach using a library called Promenade that represents computations as either successes or failures. This allows separating error handling from business logic by chaining operations and specifying success and failure handlers. It demonstrates refactoring an example application to use this approach, improving modularity and making error handling more declarative.
8. (defn valid-email-id? [email-id]
(re-matches #".+@.+..+" email-id))
(defn save-story [owner-id {:keys [email-id] :as new-story}]
(if-not (valid-email-id? email-id)
{:tag :bad-input
:message "Invalid email-id"}
(try
(let [story-id (util/clean-uuid)]
(do (db/save-story owner-id story-id new-story)
{:story-id story-id}))
(catch SQLException e
(throw (ex-info "Unable to save new story"
{:owner-id owner-id}))))))
Service errors & DB call
Valid? Input data
Insert data in DB
9. The Problems with the Convention
Spaghetti code does not scale
Refactoring becomes harder
Code is difficult to maintain
10. Lack of referential transparency
Exception-based error handling accentuates imperativeness
which makes the code brittle and hard to reason about
Composability is compromised when exceptions are thrown
What about exception handling?
15. A Mechanism:
To represent each unit of computation either a success or a
failure
Operation
Failure/Success
To decouple the result of 'if' and 'when' from 'then' or 'else'
16. Expressing Success and Failure
Source code (promenade): https://github.com/kumarshantanu/promenade
Failure may be expressed as (prom/fail failure), for example:
(ns demo-blog.web
(:require
[promenade.core :as prom]))
(prom/fail {:error "Story not found"
:type :not-found})
Any regular value that is not a Failure is considered Success.
REPL Output
#promenade.internal.Failure
{:failure {:error "Story not found",
:type :not-found}}
17. Handling Success and Failure Outcomes
Here either->> is a thread-last macro acting on the result of the previous step
A non-vector expression (list-stories) is treated as a success-handler, which is
invoked if the previous step was a success
A failure-handler is specified in a vector form: [failure-handler success-
handler] (failure->resp), which is invoked if list-stories was a failure
(prom/either->> owner-id
list-stories
[failure->resp respond-200])
18. Extending the chain
(prom/either->> owner-id
validate-input
list-stories
[failure->resp respond-200])
(prom/either->> owner-id
validate-input
list-stories
kebab->camel
[failure->resp respond-200])
Valid? Input JSON
Case conversion
Similarly we can chain together operations using macros: either-> & either-as->