Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Elegant Error-Handling for a More Civilized Age

286 views

Published on

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.

Published in: Software
  • Be the first to comment

  • Be the first to like this

Elegant Error-Handling for a More Civilized Age

  1. 1. Elegant error-handling for a more civilised age IN/Clojure'18 Varun Sharma | @var_united
  2. 2. About me Software engineer at SAP Concur Website: varunited.github.io Twitter: @var_united
  3. 3. About talk Discuss some of my learnings and experiences with error- handling. Motivation for cutting loose from some common Clojure idioms.
  4. 4. Demo app Blogging service: https://github.com/varunited/demo-blog
  5. 5. Save a new Blog post
  6. 6. Error-Handling by Convention
  7. 7. (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
  8. 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. 9. The Problems with the Convention Spaghetti code does not scale Refactoring becomes harder Code is difficult to maintain
  10. 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?
  11. 11. "The Magical Number Seven, Plus or Minus Two" - George Miller
  12. 12. (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
  13. 13. (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" {}))))))
  14. 14. How Can We Do Better?
  15. 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. 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. 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. 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->
  19. 19. (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})))
  20. 20. (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
  21. 21. 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]))
  22. 22. Conclusions
  23. 23. References: Sample app : https://github.com/varunited/demo-blog Libraries: Promenade: https://github.com/kumarshantanu/promenade Ringbelt: https://github.com/kumarshantanu/ringbelt Exception free error handling in Clojure: https://adambard.com/blog/introducing-failjure
  24. 24. Thank You!

×