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 140 days ago [-]
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.
Capricorn2481 135 days ago [-]
> so the error value is clearly visible in function signature and must at least be explicitly ignored by the caller
Not exactly. There's nothing requiring you to handle errors. But Go doesn't let you have unused values in general.
So in a transaction commit function, if you forget that it returns an error, Go will just let you ignore it. This is a big issue with treating errors as values. https://github.com/golang/go/issues/20803
ncruces 139 days ago [-]
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.
jiehong 139 days ago [-]
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 139 days ago [-]
>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 139 days ago [-]
> 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).
daveliepmann 139 days ago [-]
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.
phoe-krk 140 days ago [-]
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/
thih9 140 days ago [-]
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.
daveliepmann 140 days ago [-]
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.
NooneAtAll3 140 days ago [-]
having no experience in Closure, I was thinking exactly the latter
roenxi 140 days ago [-]
> 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.
> 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 140 days ago [-]
In a dynamically typed language, you can't even be sure you have the parts you need.
diggan 140 days ago [-]
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?
socksy 140 days ago [-]
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.
Capricorn2481 135 days ago [-]
You check the keys.
worthless-trash 140 days ago [-]
I.. know which parts I need, thats a very abstract thought pattern, what do you mean ?
MathMonkeyMan 140 days ago [-]
Not idiomatic, but it looks like [core.match][1] could be used to "if error" unpack the map:
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 139 days ago [-]
The point of `match` is you can combine the destructuring with the conditional. It's also why Rich Hickey dislikes it.
dustingetz 140 days ago [-]
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 140 days ago [-]
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 140 days ago [-]
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.
kokada 138 days ago [-]
Not sure if I understand this complain, I was a Clojure programmer and from what I remember most of the constructions in Clojure ARE defined as macros or functions.
Special forms (that are the Clojure "keywords") are small enough in number that at one time in my carrer I used to know all of them: https://clojure.org/reference/special_forms.
> and if it’s a choice between case, condp, or cond
> Finally, by not being a builtin, it doesn’t really feel blessed as a language feature
I am also quite not sure about this criticism. Lots of things that are heavily adopt by Clojure community are not built-ins but are in the `core` namespace. Probably the most (in)famous example is `core.specs`, but for example both `core.async` and `core.logic` are well used and they're still not built-ins.
dustingetz 139 days ago [-]
it is hard to criticize this library rigorously, broadly i think the second sentence here is what’s important: core.match seems designed to demonstrate the power and potential of macros, which is decidedly not the same as being designed to solve some specific problem or use case. So it has a lot of quirks and rough edges. Nobody ever really ran with it past that to see where it might lead. And whatever that is, it will be subtly different than in langs where it is the fundamental control flow construct
kokada 138 days ago [-]
Ok, I think this is a much fair opinion. It is true that I vaguely remember `core.match` being used or discussed at the time I was working with Clojure, so it could be as well that the library has too many quirks or issues to be used in a production environment.
139 days ago [-]
eduction 140 days ago [-]
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.
lbj 140 days ago [-]
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.
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 140 days ago [-]
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 140 days ago [-]
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 140 days ago [-]
: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
dustingetz 140 days ago [-]
- 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 140 days ago [-]
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 140 days ago [-]
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.”
> 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).
whalesalad 140 days ago [-]
Great post this has cleared up a lot of things for me.
daveliepmann 140 days ago [-]
I appreciate the kind words and am glad it helped :)
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.)
>If everything is idiomatic, is anything idiomatic?
Welcome to lisp! We like it here.
Not exactly. There's nothing requiring you to handle errors. But Go doesn't let you have unused values in general.
So in a transaction commit function, if you forget that it returns an error, Go will just let you ignore it. This is a big issue with treating errors as values. https://github.com/golang/go/issues/20803
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.
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.
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
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).
Tbh the latter interpretation was not one that occurred to me. Curious what you would put in such an article.
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.
https://softwareengineering.stackexchange.com/questions/2723...
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).
But as it stands, I'm not sure I understand what you mean?
[1]: https://github.com/clojure/core.match/wiki/Overview
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.
Special forms (that are the Clojure "keywords") are small enough in number that at one time in my carrer I used to know all of them: https://clojure.org/reference/special_forms.
> and if it’s a choice between case, condp, or cond
For example, those are all macros:
- case: https://github.com/clojure/clojure/blob/ce55092f2b2f5481d25c...
- condp: https://github.com/clojure/clojure/blob/ce55092f2b2f5481d25c...
- cond: https://github.com/clojure/clojure/blob/ce55092f2b2f5481d25c...
> Finally, by not being a builtin, it doesn’t really feel blessed as a language feature
I am also quite not sure about this criticism. Lots of things that are heavily adopt by Clojure community are not built-ins but are in the `core` namespace. Probably the most (in)famous example is `core.specs`, but for example both `core.async` and `core.logic` are well used and they're still not built-ins.
Checking return values for errs is a common idiom even in languages without this affordance.
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?
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.
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.)
I like having a :type key - for me usually :error/kind
- 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!
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
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).