Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Elegant Error-Handling for a more Civilized Age

Elegant Error-Handling for a more Civilized Age

We often need to deal with deeply nested conditions, complex error-handling and potentially missing data which induces software entropy. This talk covers a non-traditional approach to deal with errors where functional purity is important.

Avatar for Varun Sharma

Varun Sharma

January 13, 2018
Tweet

Other Decks in Programming

Transcript

  1. About talk Discuss some of my learnings and experiences with

    error- handling. Motivation for cutting loose from some common Clojure idioms.
  2. (defn save-story [request owner-id] (if (= (content-type? request) "application/json") (let

    [payload (->> request req/read-json-body (validate-new-story-map owner-id))] (if (contains? payload :tag) (border/err-handler payload) (let [service-response (service/save-story (util/clean-uuid owner-id) payload)] (if (contains? service-response :tag) (border/err-handler service-response) (res/json-response {:status 201 :data service-response}))))) (border/err-handler {:tag :bad-input :message "Expected content-type: application/json"}))) Web Validations Content-Type:application- json Valid? Input JSON service/save-story 201 Return story-id
  3. (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
  4. The Problems with the Convention Spaghetti code does not scale

    Refactoring becomes harder Code is difficult to maintain
  5. 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?
  6. (defn save-story [request owner-id] (if (= (content-type? request) "application/json") (let

    [payload (->> request req/read-json-body (validate-new-story-map owner-id))] (if (contains? payload :tag) (err-handler payload) (let [service-response (service/save-story (util/clean-uuid owner-id) payload)] (if (contains? service-response :tag) (err-handler service-response) (res/json-response {:status 201 :data service-response}))))) (err-handler {:tag :bad-input :message "Expected content-type: application/json"}))) Error-handling vs Business logic
  7. (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"} (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" {}))))))
  8. 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'
  9. 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}}
  10. 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])
  11. 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->
  12. (defn m-save-story [request owner-id] (prom/either->> (v/m-validate-content-type request "application/json") v/m-read-json-body-as-map (m-validate-new-story-input

    owner-id) (service/m-save-story (util/clean-uuid owner-id)) [border/failure->resp border/respond-201])) Web Validations (defn m-validate-new-story-input [owner-id {:keys [heading content email-id] :as story-map}] (if (and (s/valid? ::owner-id owner-id) (s/valid? story-spec {::heading heading ::content content ::email-id email-id})) story-map (prom/fail {:error "Bad input" :source :web :type :bad-input})))
  13. (defn m-save-story [owner-id {:keys [email-id] :as new-story}] (if-not (valid-email-id? email-id)

    (prom/fail {:error "Invalid email-id" :source :service :type :bad-input}) (try (let [story-id (util/clean-uuid)] (do (db/save-story owner-id story-id new-story) {:story-id story-id})) (catch SQLException e (prom/fail {:error "Unable to save new story" :source :execution :type :unavailable}))))) Service Errors
  14. Error-handling vs Business logic (defn m-validate-new-story-input [owner-id {:keys [heading content

    email-id] :as story-map}] (if (and (s/valid? ::owner-id owner-id) (s/valid? story-spec {::heading heading ::content content ::email-id email-id})) story-map (prom/fail {:error "Bad input" :source :web :type :bad-input}))) (defn m-save-story [request owner-id] (prom/either->> (v/m-validate-content-type request "application/json") v/m-read-json-body-as-map camel->kebab (m-validate-new-story-input owner-id) (service/m-save-story (util/clean-uuid owner-id)) kebab->camel [border/failure->resp border/respond-201]))