[HN Gopher] Idiomatic Errors in Clojure
___________________________________________________________________
Idiomatic Errors in Clojure
Author : harperlee
Score : 89 points
Date : 2024-12-12 11:15 UTC (2 days ago)
(HTM) web link (www.daveliepmann.com)
(TXT) w3m dump (www.daveliepmann.com)
| roenxi wrote:
| > if something is expected then return either nil or some {:ok
| false :message "..."} value (and {:ok true :value ...} for
| success)
|
| Maps used in this way are uncomfortable. You end up with a
| function (foo x y z) and in practice you don't know how may
| values it is about to return. Technically one, but that one might
| be a map with who-knows-what in it.
|
| There is a general API problem here of how to handle operations
| which really require multiple communication channels to report
| back with. I'm not sure if there is a good way to handle it, but
| complex objects as return value isn't very satisfying. Although
| in practice I find it works great in exceptions because the map
| is secretly just a string that is about to be logged somewhere
| and discarded.
| MarkMarine wrote:
| Maps used in this way are clojure idiom.
|
| https://softwareengineering.stackexchange.com/questions/2723...
| worthless-trash wrote:
| > but that one might be a map with who-knows-what in it.
|
| I think thatthe beauty of maps, you can take the parts you need
| and discard the rest (I am suddenly reminded of my old shifu).
| IceDane wrote:
| In a dynamically typed language, you can't even be sure you
| have the parts you need.
| diggan wrote:
| If you'd say "you can't even be sure you have the parts you
| have", it kind of makes sense, but the way we (Clojure
| devs) commonly use the REPL-in-editor kind of makes that
| moot.
|
| But as it stands, I'm not sure I understand what you mean?
| worthless-trash wrote:
| I.. know which parts I need, thats a very abstract thought
| pattern, what do you mean ?
| socksy wrote:
| Just because it's dynamically typed, doesn't mean there
| aren't types. Additionally, usage of runtime type
| enforcement such as malli schemas and core.spec are
| commonplace.
| MathMonkeyMan wrote:
| Not idiomatic, but it looks like [core.match][1] could be used
| to "if error" unpack the map: (let [result
| (do-thing x y z)] (match [result] [{:ok false
| :message msg}] {:ok false :message (format "do-thing
| failed: %s" msg)} [{:ok true, :value value}]
| (continue-computation-with value)))
|
| meh
|
| [1]: https://github.com/clojure/core.match/wiki/Overview
| roenxi wrote:
| What is the advantage there that justifies bringing in
| another dependency? You've already got a let for binding
| parts of result and could use cond from clojure.core instead
| of match. It'd be effectively identical.
| MathMonkeyMan wrote:
| The point of `match` is you can combine the destructuring
| with the conditional. It's also why Rich Hickey dislikes
| it.
| dustingetz wrote:
| i don't think core.match has worked out in practice for many
| prod projects for subtle reasons, there's a _je ne sais quoi_
| about it that seems not quite right
| kokada wrote:
| Do you have a list of those reasons? I find it curious that
| I really enjoy `match` in both Scala and Python. While it
| can be argued that Scala is a completely different beast
| than Clojure, Python is much closer (in the sense that both
| are dynamic typed languages).
| socksy wrote:
| Unfortunately (IMO), `core.match` in Clojure is a macro
| provided by a library you have to install, rather than a
| builtin function as in Scala or Python.
|
| It's a really cool demonstration of the power of lisps,
| since macros basically let you edit the code at compile
| time, rather than at runtime, and match is maybe the most
| extreme example of this as it's really compiling down the
| match statement into completely different code, rather
| than treating it like a cond statement (more info on the
| match algorithm here:
| https://github.com/clojure/core.match/wiki/Understanding-
| the...).
|
| However, when using macros there's always some trade-offs
| -- for example, you usually can't treat them as a first
| class function, passing them around (although I'm not
| sure if that's true for core.match tbf). Additionally,
| they can be confusing to debug because the code that
| you're writing doesn't match the code that's actually
| being run... stack traces can be particularly weird.
|
| Finally, by not being a builtin, it doesn't really feel
| blessed as a language feature, and if it's a choice
| between case, condp, or cond, I'm going to reach for one
| of those before core.match, simply because I don't have
| to add a new library, I can be sure that I'll understand
| the stack traces, and most importantly I can be sure that
| others will understand the code better. The fact that
| destructuring is built into Clojure means that you can
| get sort of half of the use cases for match in the first
| place, so it ends up being quite rare when you'll
| actually reach for it.
|
| I'm not sure exactly what Dustin was referring to
| exactly, but those are my gripes with it. I think it's a
| shame, because `match` is a more powerful construct every
| time I've encountered it in other languages, and it's one
| of the few things I really miss as a built in part of
| Clojure.
| eduction wrote:
| Not really, you just have to check your return values, and it
| is trivial to write a macro to make this more convenient (if-
| success, success->, etc).
|
| Checking return values for errs is a common idiom even in
| languages without this affordance.
| whalesalad wrote:
| Great post this has cleared up a lot of things for me.
| daveliepmann wrote:
| I appreciate the kind words and am glad it helped :)
| thih9 wrote:
| A bit off topic, it took me a while to figure out that the
| article is about "handling errors in clojure in an idiomatic way"
| and not "error prone clojure code that gets written so often it
| can be considered idiomatic". Especially since some of these can
| be controversial, e.g. error maps.
| NooneAtAll3 wrote:
| having no experience in Closure, I was thinking exactly the
| latter
| daveliepmann wrote:
| I considered "Idiomatic error handling in Clojure" and decided
| to err on the side of concision.
|
| Tbh the latter interpretation was not one that occurred to me.
| Curious what you would put in such an article.
| lbj wrote:
| A must-read for Clojurians.I especially appreciated that he took
| the time to comment on the correct use of assert, which is too
| often overlooked and makes debugging harder than it needs to be.
| daveliepmann wrote:
| Much appreciated. You might appreciate this follow-up on
| assertions:
| https://gist.github.com/daveliepmann/8289f0ee5b00a5f05b50379...
| dustingetz wrote:
| - regarding the bulk of these patterns, which are all just
| different encodings of error values:
|
| - the primary value prop of error values is they are concurrent,
| i.e. you can map over a collection with an effectful fn and end
| up with a collection of maybe errors (where error is encoded as
| nil, map, Either, whatever)
|
| - exceptions cannot do this
|
| - furthermore, clojure's default collection operators (mapcat
| etc) are lazy, which means exceptions can teleport out of their
| natural call stack, which can be very confusing
|
| - error values defend this
|
| - the problem is that now you have a function coloring problem:
| most functions throw, but some functions return some error
| encoding
|
| - this additional structure is difficult to balance, you're now
| playing type tetris without a type system. Clojure works best
| when you can write short, simple code and fall into the pit of
| success. Type tetris is pretty much not allowed, it doesn't
| scale, you'll regret it
|
| - you'll also find yourself with a red function deep in your
| logic that is called by a blue function, at which point you'll
| find your self doing the log-and-discard anti pattern
|
| - therefore, i agree with the first bullet: it's a hosted
| language, host exceptions are idiomatic, don't over complicate it
|
| - i do think error values can work great locally, for example
| (group-by (comp some ex-message) (map #(try-ok (f! %))), here i
| am using ex-message as a predicate. the point is you need to
| gather and rethrow asap to rejoin the language-native error
| semantics so your functions are no longer colored
|
| - i am not an authority on this, just my experience having
| explored this a bit, wrote a big system once using a monadic
| error value encoding in clojure (using the funcool either type)
| and was very unhappy. The minute you see >>= in a clojure
| codebase, it's over. (mlet is ok locally)
|
| - one thing building Electric Clojure taught me, is that the
| language/runtime can encode exception semantics "however" and
| still expose them to the user as try/catch. Which means we can
| deliver the value prop of error values under the syntax of
| try/catch.
|
| - That means, interestingly, Electric v2's exceptions are
| _concurrent_ - which means an electric for loop can throw many
| exceptions at the same time, and if some of them resolve those
| branches can resume while the others stay parked.
|
| - For Electric v3 we have not decided if we will implement
| try/catch yet, because Electric userland code is essentially
| "pure" (given that IO is managed by the runtime and resource
| effects are managed by an effect system). Userland doesn't throw,
| platform interop (database txns) is what throws, and we've found
| only very minor use cases for needing to catch that from Electric
| programs, again due to their purity. Having network failure not
| be your problem is really great for code complexity and
| abstraction!
| TacticalCoder wrote:
| I was watching a Youtube vid of yours on Electric no later than
| yesterday (mindboggling stuff)! When v3 is out of private beta,
| shall it be free / open-source?
| stefcoetzee wrote:
| From [0]: "We've historically used venture capital to fund
| Electric's development costs--4 team years, do the math!--but
| the seed market has tightened, and we realized that it's in
| everyone's interest to maintain a strong and ongoing
| investment in Electric that is decoupled from VC. That's why
| with v3 we're changing the licensing model to a source
| available business license:
|
| 1. Free "community" license for non-commercial use (e.g. FOSS
| toolmaker, enthusiast, researcher). You'll need to login to
| activate, i.e. it will "phone home" and we will receive light
| usage analytics, e.g. to count active Electric users and
| projects. We will of course comply with privacy regulations
| such as GDPR. We will also use your email to send project
| updates and community surveys, which you want to participate
| in, right?
|
| 2. Commercial use costs $480/month/developer (33% startup
| discount). No login or analytics (obviously unacceptable for
| security and privacy), instead you'll validate a license key
| (like Datomic). We also offer support and project
| implementations and are flexible with fee structure (i.e.
| services vs licenses). For free trials at work, use the
| community version with your work email. Talk to us, we will
| arm you to make the case to management.
|
| Special deal for bootstrappers: FREE until you reach $200k
| revenue or $500k funding. Just use the community license, and
| come tell us what you're doing."
|
| [0] https://tana.pub/lQwRvGRaQ7hM/electric-v3-license-change
| phoe-krk wrote:
| Also note an implementation of Common Lisp condition system in
| Clojure that allows you to have CL-style condition handling:
| https://github.com/IGJoshua/farolero/
| TacticalCoder wrote:
| They all feel kinda monadic'ish to me (and the term monad is used
| several times in TFA) and I kinda dig them when coupled with
| "enhanced" threading macros that shall short-circuit on an error
| but...
|
| How'd that all work with Clojure _spec_? I use spec and spec on
| functions ( _defn-spec_ ) all the time in Clojure. It's my way to
| keep things sane in a dynamic language (and I know quite some
| swear by spec too).
|
| I'd now need to spec, say, all my maps so that they're basically
| a "Maybe" or "Either" with the right side being my actual
| specc'ed map and the left side being specc'ed as an error dealing
| thinggy?
|
| Would that be cromulent? Did anyone try mixing such idiomatic
| error handling in Clojure mixed with specs and does it work fine?
| daveliepmann wrote:
| People's use of spec varies widely. My take is that error maps
| are for internal work. Contracts are for what passes over some
| sort of system boundary, so it seems feasible to just...not
| spec the monadic aspect?
|
| Personally I haven't worked on a project where the two
| overlapped, so perhaps someone with direct experience can chime
| in on how they navigated it.
| eduction wrote:
| For failure maps, I've found it useful to have a :tried key,
| which is the parameter that in some sense "caused" the err, or a
| map of params if there are multiple.
|
| I also usually have an error :type key.
|
| I've also found it useful to distinguish between expected errs
| and those that should end execution more quickly. Clojure allows
| hierarchical keys with "derive" so I inherit these from a top
| level error key and set them as the :type. (Why not use
| exceptions - because I've already got exit flow and error
| reporting built around the maps.)
| daveliepmann wrote:
| :tried is new to me, thanks for the tip! In the context where
| I've used this the most the entire map was the
| payload/parameter so it didn't make sense there, but I see
| where that could be useful.
|
| I like having a :type key - for me usually :error/kind
| jimbokun wrote:
| My take away:
|
| Idiomatic Clojure error handling can be idiomatic Java error
| handling, idiomatic Erlang/Elixir error handling, or idiomatic
| Haskell error handling.
|
| If everything is idiomatic, is anything idiomatic?
|
| (This article also makes me strangely appreciative of Go's
| idiomatic error handling. Use multiple return values, so the
| error value is clearly visible in function signature and must at
| least be explicitly ignored by the caller. Avoids the action at a
| distance of exceptions, and the invisibility of errors in the
| dynamic approaches recommended in the article.)
| daveliepmann wrote:
| Throwing ex-info is conspicuously missing from your list of
| examples.
|
| >If everything is idiomatic, is anything idiomatic?
|
| Welcome to lisp! We like it here.
| ncruces wrote:
| Java's exceptions are also visible in the signature, and harder
| to ignore.
|
| I actually like Go's solution better, _because_ it 's verbose,
| because the fact that I pay the cost of "if err != nil" every
| effin time, I'm more likely to consider what happens.
|
| Because if the flow is not linear with those, I'm more likely
| to break out a function and give it a name.
| daveliepmann wrote:
| The "error maps" approach from the article doesn't have Go's
| compiler enforcement, but shares many attributes including
| the "being explicit about how to handle an error" aspect you
| mention.
| jiehong wrote:
| Go error handling is nice, but not super mandatory.
|
| I've seen functions only returning an error being called
| without the error being assigned to anything and never
| checked. The compiler does not care, only go ci lint would
| block that.
|
| I've also seen a function returning a value and and error,
| and the error being mapped to _ on the caller side. Compiler
| and linter are fine with that (and sometimes it's mandatory
| with some weird libraries).
|
| Lastly, a nitpick: should functions return (value, error, or
| (error, value)? The former is a convention, but the latter
| happens sometimes in some libraries.
| daveliepmann wrote:
| >should functions return (value, error), or (error, value)?
|
| a benefit of using maps with error keys :D semantic rather
| than positional destructuring. also avoids that needlessly
| obfuscatory Left and Right terminology for Either monads
| ncruces wrote:
| > I've also seen a function returning a value and and
| error, and the error being mapped to _ on the caller side.
| Compiler and linter are fine with that (and sometimes it's
| mandatory with some weird libraries).
|
| Yes, that's the way to _explicitly_ ignore the error.
| Ignoring the result, instead of attributing to the blank
| identifier _ , is bad form (unless you 're doing something
| like defer a Close).
| layer8 wrote:
| > It's common to see (catch Throwable) sprinkled liberally across
| a Clojure codebase.
|
| Just like (catch Exception), this also breaks the semantics of
| _InterruptedException_ , which (to maintain its semantics) either
| has to be rethrown, or the catching code has to set the the
| current thread's interrupt flag ( _Thread::interrupt_ ).
___________________________________________________________________
(page generated 2024-12-14 23:01 UTC)