Fun With Errors?
Clojure Finland Meetup, MOW@Tampere
Tommi Reiman
tommi@metosin.
@ikitommi
Who is interested in errors?
Developers <-- you, me!, the girl/guy next to you?
Programs
Systems
End Users
Clojure & Error Messages
Bare-bones before 1.9.0
Really bad with 1.9.0 (clojure.spec)
Kinda ok in 1.10.0 (data + helpers)
1.9.0
(ns kikka
(require '[clojure.string]))
; CompilerException clojure.lang.ExceptionInfo: Call to clojure.
; did not conform to spec: In: [1] val: ((require (quote [clojur
; fails spec: :clojure.core.specs.alpha/ns-form at: [:args] pred
; (cat :docstring (? string?) :attr-map (? map?) :clauses :cloju
; Extra input #:clojure.spec.alpha{:problems [{:path [:args], :r
; "Extra input", :pred (clojure.spec.alpha/cat :docstring (cloju
; clojure.core/string?) :attr-map (clojure.spec.alpha/? clojure.
; :clojure.core.specs.alpha/ns-clauses), :val ((require (quote [
; :via [:clojure.core.specs.alpha/ns-form], :in [1]}], :spec #ob
; "clojure.spec.alpha$regex_spec_impl$reify__2436@3c18dcbf"], :v
; (kikka (require (quote [clojure.string]))), :args (kikka (requ
; [clojure.string])))}, compiling:(/Users/tommi/projects/metosin
1.10.0
(ns kikka
(require '[clojure.string]))
; Syntax error macroexpanding clojure.core/ns at
; (spec.cljc:141:1). ((require (quote [clojure.string]))) -
; failed: Extra input spec: :clojure.core.specs.alpha/ns-form
New helpers in 1.10.0
Throwable->map , ex-triage , ex-str
clojure.main/repl with :caught option
https://clojure.org/reference/repl_and_main
New ex-data in 1.10.0
:clojure.error/phase - phase indicator
:clojure.error/source - le name (no path)
:clojure.error/line - integer line number
:clojure.error/column - integer column number
:clojure.error/symbol - symbol being
expanded/compiled/invoked
:clojure.error/class - cause exception symbol
:clojure.error/cause - cause exception message
:clojure.error/spec - explain-data for a spec error
ETA
ELM
Clojure 3rd party thingies
IDEs & Linters
Kibit (https://github.com/jonase/kibit)
Eastwood (https://github.com/jonase/eastwood)
Cursive (https://cursive-ide.com/)
Joker (https://github.com/candid82/joker)
CLJ-Kondo (https://github.com/borkdude/clj-
kondo)
Pretty (developers)
https://github.com/AvisoNovate/pretty
Schema
https://github.com/plumatic/schema
(app {:request-method :post
:uri "/api/plus"
:query-params {"x" "1", "y" "-7"}})
; {:status 500,
; :body {:schema {:total "(constrained Int PositiveInt)"},
; :errors {:total "(not (PositiveInt -6))"},
; :type :reitit.coercion/response-coercion,
; :coercion :schema,
; :value {:total -6},
; :in [:response :body]}}
clojure.spec
(require '[clojure.spec.alpha :as s])
(s/def ::city string?)
(s/def ::state string?)
(s/def ::place (s/keys :req-un [::city ::state]))
(s/explain-data ::place {:city "Denver", :state :CO})
;(:problems ({:path [:state],
; :pred clojure.core/string?,
; :val :CO,
; :via [::place ::state],
; :in [:state]}),
; :spec :spec-tools.data-spec-test/place,
; :value {:city "Denver", :state :CO} }
Expound 1/2
https://github.com/bhb/expound
(require '[expound.alpha :as expound])
(expound/expound ::place {:city "Denver", :state :CO})
;; -- Spec failed --------------------
;;
;; {:city ..., :state :CO}
;; ^^^
;;
;; should satisfy
;;
;; string?
;;
;; -------------------------
;; Detected 1 error
Expound 2/2
Ships with custom pretty printer
Spell-spec
https://github.com/bhauman/spell-spec
spell-spec.alpha/strict-keys
Metosin Public Corner
Integrating spell-spec with spec-tools for closed
validation of spec'd con gurations (with
@bhauman-grade error reporting)
metosin/reitit is a playground of joyful things,
including data, performance and... error messages!
Some Schema too?
Ingredients
Fipp, fast Idiomatic Pretty-Printer
https://github.com/brandonbloom/ pp
Expound & Spell-Spec
Lovely colors from rebel-readline
https://github.com/bhauman/rebel-readline
Our libraries (spec-tools & friends)
Principles
Data-oriented libraries & tools
Design to Fail-fast
Good developer experience
Lead by Example
Towards clj-commons error formatter?
Fail Fast
During static analysis (e.g. linter)
At compile-time (macros, defs)
At creation-time
At development-time (schema/spec annos)
At runtime ( )
On Component 1/2
Clean separation of creation and request-time
Support declarative validation at creation-time
Correct Initialization, Fast Runtime
(defn coerce-request-interceptor
"Interceptor for pluggable request coercion.
Expects a :coercion of type `reitit.coercion/Coercion`
and :parameters, otherwise does not mount."
[]
{:name ::coerce-request
:spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts]
...)})
On Component 2/2
Schema is great at validating function args:
clojure.spec has fdef and s/assert
(require '[schema.core :as s])
(s/defn ^:always-validate interceptor-x
[opts :- {:string? s/Bool}]
...)
(coerce-request-interceptor {})
; Syntax error (ExceptionInfo) compiling at (test.cljc:150:1).
; Input to interceptor-x does not match schema:
;
; [(named {:string? missing-required-key} opts)]
On Error
No formatting here, just emit a unique id for the
error and all the needed info for print the error:
(when-let [problems (validate-route-data routes spec)]
(exception/fail!
::invalid-route-data
{:problems problems}))
On (Router) Creation
Before there is a way to hook a custom exception
handler into a running REPL, we just catch the
exceptions ourself on the public api and use a
con gured formatter for the errors:
(defn router [raw-routes opts]
(try
...
(catch #?(:clj Exception, :cljs js/Error) e
(throw ((get opts :exception identity) e)))))
Formatting (DEMO)
Formatting Guide
Respect the user
Use lame colors, it's not xmas
Just simple tips (or none)
Link to documentation (if needed)
On route con ict 1/2
(require '[reitit.core :as r])
(require '[reitit.dev.pretty :as pretty])
(r/router
[["/ping"]
["/:user-id/orders"]
["/bulk/:bulk-id"]
["/public/*path"]
["/:version/status"]]
{:exception pretty/exception})
On route con ict 2/2
Invalid Route Data 1/2
(require '[reitit.spec :as spec])
(require '[clojure.spec.alpha :as s])
(s/def ::role #{:admin :user})
(s/def ::roles (s/coll-of ::role :into #{}))
(r/router
["/api/admin" {::roles #{:adminz}}]
{:validate spec/validate
:exception pretty/exception})
Invalid Route Data 2/2
All solved?
Challenges
clojure.spec is open by design
enables growth (and typos!)
clojure.spec is macros (Spec2 adds functions)
clojure.spec doesn't support rt-transformations
Spec2 is xing some of the things, WIP
(Remember Schema, anyone?)
Batteries to clojure.spec ?
Ability to deep-merge (map) specs
Ability to close (map) specs
s/select in Spec2 looks promising (WIP, TBD):
(s/select
::user
[::first ::last ::addr
{::addr [::street ::city ::state ::zip]}])
While waiting
spec-tools.spell will have functions to close specs
Work nicely with data-specs (DEMO)
Might work with normal specs using Coercion
https://cljdoc.org/d/metosin/spec-
tools/0.9.1/doc/spec-coercion
Will integrate to reitit if that ever works
st/defn to help validating component options?
Final Words
Clojure is a Dynamic language -> Fail Fast(!!!)
Fast linters coming ( joker & clj-kondo )
Ongoing work to make reitit awesome
Fipp, Expound, spell-spec & spec-tools
Community should build a kick-ass error
formatter & rules for Clojure, Made in ?
Join #reitit & #clj-commons in Slack
join@metosin.
Thanks.

Fun with errors? - Clojure Finland Meetup 26.3.2019 Tampere

  • 1.
    Fun With Errors? ClojureFinland Meetup, MOW@Tampere Tommi Reiman tommi@metosin. @ikitommi
  • 2.
    Who is interestedin errors? Developers <-- you, me!, the girl/guy next to you? Programs Systems End Users
  • 3.
    Clojure & ErrorMessages Bare-bones before 1.9.0 Really bad with 1.9.0 (clojure.spec) Kinda ok in 1.10.0 (data + helpers)
  • 4.
    1.9.0 (ns kikka (require '[clojure.string])) ;CompilerException clojure.lang.ExceptionInfo: Call to clojure. ; did not conform to spec: In: [1] val: ((require (quote [clojur ; fails spec: :clojure.core.specs.alpha/ns-form at: [:args] pred ; (cat :docstring (? string?) :attr-map (? map?) :clauses :cloju ; Extra input #:clojure.spec.alpha{:problems [{:path [:args], :r ; "Extra input", :pred (clojure.spec.alpha/cat :docstring (cloju ; clojure.core/string?) :attr-map (clojure.spec.alpha/? clojure. ; :clojure.core.specs.alpha/ns-clauses), :val ((require (quote [ ; :via [:clojure.core.specs.alpha/ns-form], :in [1]}], :spec #ob ; "clojure.spec.alpha$regex_spec_impl$reify__2436@3c18dcbf"], :v ; (kikka (require (quote [clojure.string]))), :args (kikka (requ ; [clojure.string])))}, compiling:(/Users/tommi/projects/metosin
  • 5.
    1.10.0 (ns kikka (require '[clojure.string])) ;Syntax error macroexpanding clojure.core/ns at ; (spec.cljc:141:1). ((require (quote [clojure.string]))) - ; failed: Extra input spec: :clojure.core.specs.alpha/ns-form
  • 6.
    New helpers in1.10.0 Throwable->map , ex-triage , ex-str clojure.main/repl with :caught option https://clojure.org/reference/repl_and_main
  • 7.
    New ex-data in1.10.0 :clojure.error/phase - phase indicator :clojure.error/source - le name (no path) :clojure.error/line - integer line number :clojure.error/column - integer column number :clojure.error/symbol - symbol being expanded/compiled/invoked :clojure.error/class - cause exception symbol :clojure.error/cause - cause exception message :clojure.error/spec - explain-data for a spec error
  • 9.
  • 10.
  • 11.
  • 12.
    IDEs & Linters Kibit(https://github.com/jonase/kibit) Eastwood (https://github.com/jonase/eastwood) Cursive (https://cursive-ide.com/) Joker (https://github.com/candid82/joker) CLJ-Kondo (https://github.com/borkdude/clj- kondo)
  • 13.
  • 14.
    Schema https://github.com/plumatic/schema (app {:request-method :post :uri"/api/plus" :query-params {"x" "1", "y" "-7"}}) ; {:status 500, ; :body {:schema {:total "(constrained Int PositiveInt)"}, ; :errors {:total "(not (PositiveInt -6))"}, ; :type :reitit.coercion/response-coercion, ; :coercion :schema, ; :value {:total -6}, ; :in [:response :body]}}
  • 15.
    clojure.spec (require '[clojure.spec.alpha :ass]) (s/def ::city string?) (s/def ::state string?) (s/def ::place (s/keys :req-un [::city ::state])) (s/explain-data ::place {:city "Denver", :state :CO}) ;(:problems ({:path [:state], ; :pred clojure.core/string?, ; :val :CO, ; :via [::place ::state], ; :in [:state]}), ; :spec :spec-tools.data-spec-test/place, ; :value {:city "Denver", :state :CO} }
  • 16.
    Expound 1/2 https://github.com/bhb/expound (require '[expound.alpha:as expound]) (expound/expound ::place {:city "Denver", :state :CO}) ;; -- Spec failed -------------------- ;; ;; {:city ..., :state :CO} ;; ^^^ ;; ;; should satisfy ;; ;; string? ;; ;; ------------------------- ;; Detected 1 error
  • 17.
    Expound 2/2 Ships withcustom pretty printer
  • 18.
  • 20.
    Metosin Public Corner Integratingspell-spec with spec-tools for closed validation of spec'd con gurations (with @bhauman-grade error reporting) metosin/reitit is a playground of joyful things, including data, performance and... error messages! Some Schema too?
  • 21.
    Ingredients Fipp, fast IdiomaticPretty-Printer https://github.com/brandonbloom/ pp Expound & Spell-Spec Lovely colors from rebel-readline https://github.com/bhauman/rebel-readline Our libraries (spec-tools & friends)
  • 22.
    Principles Data-oriented libraries &tools Design to Fail-fast Good developer experience Lead by Example Towards clj-commons error formatter?
  • 23.
    Fail Fast During staticanalysis (e.g. linter) At compile-time (macros, defs) At creation-time At development-time (schema/spec annos) At runtime ( )
  • 24.
    On Component 1/2 Cleanseparation of creation and request-time Support declarative validation at creation-time Correct Initialization, Fast Runtime (defn coerce-request-interceptor "Interceptor for pluggable request coercion. Expects a :coercion of type `reitit.coercion/Coercion` and :parameters, otherwise does not mount." [] {:name ::coerce-request :spec ::rs/parameters :compile (fn [{:keys [coercion parameters]} opts] ...)})
  • 25.
    On Component 2/2 Schemais great at validating function args: clojure.spec has fdef and s/assert (require '[schema.core :as s]) (s/defn ^:always-validate interceptor-x [opts :- {:string? s/Bool}] ...) (coerce-request-interceptor {}) ; Syntax error (ExceptionInfo) compiling at (test.cljc:150:1). ; Input to interceptor-x does not match schema: ; ; [(named {:string? missing-required-key} opts)]
  • 26.
    On Error No formattinghere, just emit a unique id for the error and all the needed info for print the error: (when-let [problems (validate-route-data routes spec)] (exception/fail! ::invalid-route-data {:problems problems}))
  • 27.
    On (Router) Creation Beforethere is a way to hook a custom exception handler into a running REPL, we just catch the exceptions ourself on the public api and use a con gured formatter for the errors: (defn router [raw-routes opts] (try ... (catch #?(:clj Exception, :cljs js/Error) e (throw ((get opts :exception identity) e)))))
  • 28.
  • 29.
    Formatting Guide Respect theuser Use lame colors, it's not xmas Just simple tips (or none) Link to documentation (if needed)
  • 30.
    On route conict 1/2 (require '[reitit.core :as r]) (require '[reitit.dev.pretty :as pretty]) (r/router [["/ping"] ["/:user-id/orders"] ["/bulk/:bulk-id"] ["/public/*path"] ["/:version/status"]] {:exception pretty/exception})
  • 31.
    On route conict 2/2
  • 32.
    Invalid Route Data1/2 (require '[reitit.spec :as spec]) (require '[clojure.spec.alpha :as s]) (s/def ::role #{:admin :user}) (s/def ::roles (s/coll-of ::role :into #{})) (r/router ["/api/admin" {::roles #{:adminz}}] {:validate spec/validate :exception pretty/exception})
  • 33.
  • 34.
  • 35.
    Challenges clojure.spec is openby design enables growth (and typos!) clojure.spec is macros (Spec2 adds functions) clojure.spec doesn't support rt-transformations Spec2 is xing some of the things, WIP (Remember Schema, anyone?)
  • 36.
    Batteries to clojure.spec? Ability to deep-merge (map) specs Ability to close (map) specs s/select in Spec2 looks promising (WIP, TBD): (s/select ::user [::first ::last ::addr {::addr [::street ::city ::state ::zip]}])
  • 37.
    While waiting spec-tools.spell willhave functions to close specs Work nicely with data-specs (DEMO) Might work with normal specs using Coercion https://cljdoc.org/d/metosin/spec- tools/0.9.1/doc/spec-coercion Will integrate to reitit if that ever works st/defn to help validating component options?
  • 39.
    Final Words Clojure isa Dynamic language -> Fail Fast(!!!) Fast linters coming ( joker & clj-kondo ) Ongoing work to make reitit awesome Fipp, Expound, spell-spec & spec-tools Community should build a kick-ass error formatter & rules for Clojure, Made in ? Join #reitit & #clj-commons in Slack join@metosin.
  • 40.