- I always think more languages should support Result… but only to handle expected error states. For example, you may expect that some functions may time out or that a user may attempt an action with an invalid configuration (e.g., malformed JSON).
Exceptions should be reserved for developer errors like edge cases that haven’t been considered or invalid bounds which mistakenly went unchecked.
- I find it kind of funny that this is almost exactly how Java's much-maligned "checked exceptions" work. Everything old is new again.
In Java, when you declare a function that returns type T but might also throw exceptions of type A or B, the language treats it as though the function returned a Result<T, A|B>. And it forces the caller to either handle all possible cases, or declare that you're rethrowing the exception, in which case the behavior is the same as Rust's ? operator. (Except better, because you get stack traces for free.)
- Java's distinction between Runtime and Checked Exception makes sense, and is pretty much the same panic vs Result distinction Rust makes. But Java's execution of the concept is terrible.
1. Checked exception don't integrate well with the type-system (especially generics) and functional programming. It's also incompatible with creating convenient helper functions, like Rust offers on Result.
2. Converting checked exceptions into runtime exception is extremely verbose, because Java made the assumption that the type of error distinguishes between these cases. While in reality errors usually start as expected in low-level functions, but become unexpected at a higher level. In Rust that's a simple `unwrap`/`expect`. Similarly converting a low level error type to a higher level error type is a simple `map_err`.
3. Propagation of checked exception is implicit, unlike `?` in Rust
Though Rust's implementation does have its weaknesses as well. I'd love the ability to use `Result<T, A | B>` instead of needing to define a new enum type.
- I wish I could upvote this more. I can totally understand GP's sentiment, but we need to dispel the myth that results are just checked exceptions with better PR.
I think the first issue is the most important one, and this is not just an implementation issue. Java eschewed generics on its first few versions. This is understandable, because generics were quite a new concept back then, with the only mainstream languages implementing them then being Ada and C++ - and the C++ implementation was brand new (in 1991), and quite problematic - it wouldn't work for Java. That being said, this was a mistake in hindsight, and it contributed to a lot of pain down the road. In this case, Java wanted to have exception safety, but the only way they could implement this was as another language feature that cannot interact with anything else.
Without the composability provided by the type system, dealing with checked exceptions was always a pain, so most Java programmers just ended up wrapping them with runtime exceptions. Using checked exceptions "correctly" meant extremely verbose error handling with a crushing signal-to-noise ratio. Rust just does this more ergonomically (especially with crates like anyhow and thiserror).
- You know, I’ve also found this funny.
I like the declaration side. I think part of where it misses the mark is the syntax on the caller side.
I feel like standard conditionals are enough to handle user errors while the heavy machinery of try-catch feels appropriately reserved for unexpected errors.
- Probably, the problem with Java's `try-catch` is it's not composable and has an abundance of unchecked exceptions (could mess up `catch` type). In Rust, you could just `?` to short-circuit return or do another chained method call `result.map(...).and_then(...).unwrap_or(...)`.
And more importantly, I don't think there's any JEP trying to improve checked exception handling.
- While Java gets the blame, the concept was already present in CLU, Modula-3 and C++ before Java was even an idea.
I also find a certain irony that forced checked results are exactly the same idea from CS type theory point of view, even if the implementation path is a different one.
- Java's checked exceptions experiment was very painful in various ways that directly exposing an error state as part of the return value is not so I wouldn't quite characterize this as "Everything old is new again."
The first big thing is that Java, especially in the days of when checked exceptions were a really big thing and less so in modern Java, was really into a certain kind of inheritance and interface design that didn't play well with error states and focused on the happy path. It is very difficult to make Java-esque interfaces that play well with checked exceptions because they like to abstract across network calls, in-memory structures, filesystem operations, and other side effectful tasks that have very different exception structures. An interface might have a single `writeData` method that might be backed by alternatively a write into an in-memory dictionary, a filesystem key-value store, a stateless REST API, or a bidirectional WebSocket channel which all have wildly different exceptions that can occur.
The second thing is that because checked exceptions were not actual return values but rather had their own special channel, they often did not play well with other Java API decisions such as e.g. streams or anything with `Runnable` that involved essentially the equivalent of a higher-order function (a function that takes as an argument another function). If e.g. you had something you wanted to call in a `Stream.map` that threw a checked exception, you couldn't use it, even if you notated in the enclosing method that you were throwing a checked exception because there was no way of telling `Stream.map` "if the function being `map`ed throws an exception rethrow it" which arose because checked exceptions weren't actual return values and therefore couldn't be manipulated the same way. You could get around it, but would have to resort to some shenanigans that would need to be repeated every time this issue came up for another API.
On the other hand if this wasn't a checked exception but was directly a part o the return value of a function, it would be trivial to handle this through the usual generics that Java has. And that is what something like `Result` accomplishes.
- IMHO the mapping issue comes from functions not being first class, so all types require Functor-like interfaces which are needlessly verbose. Splitting these is not semantically different than a function that returns a value vs a function that returns a Result.
I have little love for Java, but explicitly typed checked exceptions are something I miss frequently in other languages.
- No I think it's a deeper issue than that. In particular because exceptions aren't a return value, you can't make a function generic over both values and exceptions at the same time. This would persist even with first class functions.
If you want to be generic over exceptions, you have to throw an exception. It would be nice to have e.g. a single `map` method that appropriately throws an exception when the function that is called throws a function and one that doesn't throw an exception when the function that is called throws a function. But as it stands, if you want to be able to throw a checked exception at all, you have to mark that your higher order function throws checked exceptions, even if you would prefer to be more generic so that e.g. you only throw a checked exception if your function that is called throws a checked exception.
- Only it is not considered by the type checker. Result brings errors into the realm of properly typed code that you can reason about. Checked exceptions are a bad idea that did not work out (makes writing functional code tedious, messes with control flow, exceptions are not in the type system).
- The only difference between a `fun doThing: Result<X, SomeError>` and a `fun doThing: X throws SomeError` is that with the checked exception, unpacking of the result is mandatory.
You're still free to wrap the X or SomeError into a tuple after you get one or other other. There is no loss of type specificity. It is no harder to "write functional code" - anything that would go in the left() gets chained off the function call result, and anything that would go in the right() goes into the appropriate catch block.
- I also don’t understand the argument that Result is anything other than a syntactic difference between these ideas.
vs.final Foo x; try { x = foo().bar().baz().car(); } catch (Exception e) { x = null; } return Optional.of(x);
Both eat the error. Both wrap the value. The rust is more terse, but the meaning is the same.let x = foo()?.bar()?.baz()?.car()?; Some(x)- You've discarded the error type, which trivialised the example. Rust's error propagation keeps the error value (or converts it to the target type).
The difference is that Result is a value, which can be stored and operated on like any other value. Exceptions aren't, and need to be propagated separately. This is more apparent in generic code, which can work with Result without knowing it's a Result. For example, if you have a helper that calls a callback in parallel on every element of an array, the callback can return Result, and the parallel executor doesn't need to care (and returns you an array of results, which you can inspect however you want). OTOH with exceptions, the executor would need to catch the exception and store it somehow in the returned array.
- Try:
I have rewritten the parent code to preserve the exception without semantic difference or loss of type safety.Either<Foo, SomeException> x; try { x = Either.left(foo().bar().baz().car()); } catch (SomeException e) { x = Either.right(e); }If there are multiple types of exception that can be thrown, the right can be a union type.
- Either is another name for a Result type.
Either works, but now you have two ways of returning errors, and they aren't even mutually exclusive (Either-retuning function can still throw).
Catch-and-wrap doesn't compose in generic code. When every call may throw, it isn't returning its return type T, but actually an Either<T, Exception>, but you lack type system capable of reasoning about that explicitly. You get an incomplete return type, because the missing information is in function signatures and control flow, not the types they return. It's not immediately obvious how this breaks type systems if you keep throwing, but throwing stops being an option when you want to separate returning errors from the immediate act of changing control flow, like when you collect multiple results without stopping on the first error. Then you need a type for capturing the full result type of a call.
If you write a generic map() function that takes T and returns U, it composes well only if exceptions don't alter the types. If you map A->B, B->C, C->D, it trivially chains to A->D without exceptions. An identity function naturally gives you A->A mapping. This works with Results without special-casing them. It can handle int->Result, Result->int, Result->Result, it's all the same, universally. It works the same whether you map over a single element, or an array of elements.
But if every map callback could throw, then you don't get a clean T->U mapping, only T -> Either<U, Exception>. You don't have an identity function! You end up with Either<Either<Either<... when you chain them, unless you special-case collapsing of Eithers in your map function. The difference is that with Result, any transformation of Result<T, E1> to Result<U, E2> (or any other combo) is done inside the concrete functions, abstracted away from callers. But if a function call throws, the type change and transformation of the type is forced upon the caller. It can't be abstracted away from the caller. The map() needs to know about Either, and have a strategy for wrapping and unwrapping them.
catch lets you convert exceptions to values, and throw convert values to exceptions, so in the end you can make it work for any specific use-case, but it's just this extra clunky conversion step you have to keep doing, and you juggle between two competing designs that don't compose well. With Result, you have one way of returning errors that is more general, more composable, and doesn't have a second incomplete less flexible way to be converted to/from.
- I think you're missing the key point about return types with checked exceptions.
`int thing()` in Java returns type `int`. `int thing() throws AnException` in Java returns type `int | AnException`, with language-mandated destructuring assignment with the `int` going into the normal return path and `AnException` going into a required `catch` block.
`int thing() throws AnException, OtherException` returns type `int | AnException | OtherException`.
The argument you're making, that the compiler doesn't know the return type and "you lack type system capable of reasoning about that explicitly", is false. Just because the function says its return type is `int` doesn't mean the compiler is unaware there are three possible returns, and also doesn't mean the programmer is unaware of that.
The argument you are making applies to UNchecked exceptions and does not apply to CHECKED exceptions.
- It's not a single return type T that is a sum type. It's two control flow paths returning one type each, and that's a major difference, because the types and control flow are complected together in a way that poorly interacts with the type system.
It's not `fn(T) -> U` where U may be whatever it wants, including Ok|Exception in some cases. It's `fn(T) -> U throws E`, and the `U throws E` part is not a type on its own. It's part of the function signature, but lacks a directly corresponding type for U|E values. It's a separate not-a-type thing that doesn't exist as a value, but is an effect of control flow changes. It needs to be caught and converted to a real value with a nameable type before it can work like a value. Retuning Either<U, E> isn't the `U throws E` thing either. Java's special alternative way of returning either U or E is not a return type, but two control flow paths returning one type each.
Compiler is fully aware of what's happening, but it's not the same mechanism as Result. By focusing on "can this be done at all", you miss the whole point of Result achieving this in a more elegant way, with fewer special non-type things in the language. Being just a regular value with a real type, which simply works everywhere where values work without altering control flow is the main improvement of Result over checked exceptions. Removal of try/catch from the language is the advantage and simplification that Result brings.
Result proves that Java's special-case exception checking is duplicating work of type checking, which needlessly lives half outside of the realm of typed values. Java's checked exceptions could be removed from the language entirely, because it's just a duplicate redundant type checker, with less power and less generality than the type checker for values.
- I usually divide things in "errors" (which are really "invariant violations") and "exceptions". "exceptions", as the name implies, are exceptional, few and when they happen, they're usually my fault, "errors" on the other hand, depending on the user, happens a lot and usually surfaced to the user.
- why not divide things into errors and bugs (programming errors)?
- Both bugs and exceptions can be reasonably thought of as programmer error, but they are not the same kind of programmer error. Exceptions are flaws in the code — conditions that could have been caught at compile time with a sufficiently advanced language/compiler. Whereas bugs are conditions that are programatically sound, but violate human expectations.
- A little nuance: bugs are not just conditions that are programmatically sound. They can encompass exceptions.
If a bug triggers an exception then with a strong compiler that is sufficiently advanced then these bugs can be found by the compiler.
- Bugs require execution so a compiler cannot find bugs.
Exceptions also require execution, but that does not suggest that they are encompassed by bugs. The lack of a third term tells that there is no overlap. If "bug" covered both exceptions and where human expectations are violated, there would necessarily be another term just for the case where human expectations are violated. But there is no such term...
...that I've ever heard. If it is that I've been living under a rock, do tell.
- No you haven’t been living under a rock your definitions are just off and you didn’t read carefully what I wrote. Or you’re just overly pedantic.
I wrote bugs can cover exceptions. Think hard about what that sentence means in English. If I can do something it means I can both do something and not do something. So that means there are exceptions that are bugs and exceptions that are not bugs.
The reason why it’s important to illustrate this difference is because a large, large number of exceptions occur as a bug.
- > So that means there are exceptions that are bugs
Which, like before, is false. Are you, perhaps, confusing exceptions (the concept; what we're talking about here) with the data structure of the same name? A bug could erroneously lead to the creation of an exception (data structure), but that wouldn't be an exception (concept), that'd just be a bug.
There is no situation where an exception (concept) is also a bug, unless you take "bug" to mean something akin to general "programmer error", where exceptions are subset thereof. But, as before, we know that's not how "bug" is used as there is no other terminology for human expectation violations to fill the void. No need as that is already what "bug" refers to.
> a large, large number of exceptions occur as a bug.
That doesn't really make any sense. Maybe if, again, you have confused exceptions (concept) with exceptions (data structure), then it could start to mean something, but even then "large, large" is one hell of a claim. Let's be real: The vast majority of exceptions (data structure) are created by programmers who mistakenly believe that "exception" is another word for "error". While not exactly a good idea, that use falls under neither exceptions (concept) nor bugs. In second place, exceptions (data structure) are created due to exceptions (concept). I'm sure exceptions (data structure) being created due to bugs has happened before, but I've never seen it.
- You’re arguing as though reality cares about your word boundaries. Saying “exceptions are not bugs” because one is a concept and the other is a data structure is like saying a flat tire isn’t a car problem because the air is outside the vehicle. It’s technically true in the same way “rain is just water, not weather” is true. Nobody outside the argument needs a whiteboard to see what’s gone wrong.
An exception is what happens when a bug hits the runtime. The program trips, falls, and throws a message about it. Calling that “not a bug” is like saying your toaster catching fire isn’t a malfunction because combustion is a physical process, not an electrical one. Or saying a sinking ship isn’t in trouble because “leak” is a noun and “disaster” is a category.
In practice, exceptions are how bugs announce their presence. You can keep redrawing the philosophical fence around which one counts as which, but it’s the same as a child insisting the spilled milk isn’t a mess because it’s just “liquid on the table.” Everyone else has already grabbed the paper towels.
- > An exception is what happens when a bug hits the runtime.
Remember, "exception" is short for "exceptional event". "An exceptional event is what happens when a bug hits the runtime." means... what, exactly? And let's not forget that you said that not all exceptions are bugs so it seems we also have "An exceptional event is what happens when a bug does not hit the runtime." What is that supposed to mean?
Returning us from la-la land, an exceptional event, or exception, refers to encountering a fundamental flaw in the instruction. Something like divide by zero or accessing an out of bounds index in an array. Things that you would never have reason to carry out and that a more advanced compiler could have reasonably alerted you to before execution.
Bugs are different. They can only be determined under the watchful eye of a human deciding that something isn't behaving correctly. Perhaps your program has a button that is expected to turn on an LED, but instead it launches a nuclear missile. While that is reasonably considered programmer error, that's not a fundamental flaw — that's just not properly capturing the human intent. In another universe that button launching nuclear missiles very well could be the intended behaviour. There is no universe where accessing an out of bounds index is intended.
> exceptions are how bugs announce their presence
Bugs aren't announceable, fundamentally. They can be noticed by a human who understands the intent of the software, but it is impossible for the machine to determine what is and what isn't a bug. The machine can determine what is an exception, though, and that's why exceptions often get special data structures and control flows in programming languages while bugs don't.
- You’re still mistaking your own definitions for truth. You’re describing programming as if words create reality, not the other way around. Your claim that bugs “can only be determined by humans” and “machines cannot announce them” is like saying a car cannot have a problem until a mechanic gives it a name. The smoke pouring from the hood does not wait for linguistic permission to exist.
Exceptions are the runtime form of program faults. The fact that we can construct them synthetically or that some are anticipated does not erase their relationship to bugs. You seem to believe that because we can imagine a world where launching a missile is “intended,” it stops being a bug. By that logic, nothing in computing can ever be wrong as long as someone somewhere hypothetically wanted it that way. That isn’t philosophy. It’s a child’s loophole.
Your “fundamental flaw in the instruction” definition collapses immediately under reality. A division by zero is only “fundamental” because of human intent: we chose to define arithmetic that way. Under your logic, if we wrote a compiler that quietly handled it as infinity, exceptions would vanish from the universe. That should tell you that your ontology is not describing truth but just the current convention of language design.
The machine can’t “determine” bugs? Of course it can’t. The machine can’t determine anything. It executes. Yet you just admitted exceptions exist because “the machine determines them.” You’ve built a castle out of circular definitions and are calling it a worldview.
In practice, exceptions are one of the main observable ways that bugs manifest. The rest of us live in the world where programs crash, not in the world where we rename crashes until they sound academic.
- > [...] is like saying a car cannot have a problem until a mechanic gives it a name. The smoke pouring from the hood does not wait for linguistic permission to exist.
That doesn't make any sense. But there may be something salvageable in your analogy. Consider the difference between manufacturing defects and wear and tear. You can understand how each, while both able to lead to failure, are different categories of problems?
Good. Now, analogies can only go so far, but we can extrapolate some similarity from that in software. Meaning that both bugs and exceptions are problems, but they are not of the same category. They originate from different conditions. Exceptions are fundamental flaws in the code, while bugs are deviation from human intent.
> Yet you just admitted exceptions exist because “the machine determines them.”
That's right. The machine can determine when an exception occurs. For example, divide by zero. There is no way the machine can carry on once this happens. The machine is capable of addressing such a condition. Which is very different from divide by one when you actually intended to divide by two. This is what differentiates exceptions from bugs and why we have more than one word to describe these things.
> exceptions are one of the main observable ways that bugs manifest.
Maybe you're still confusing the concept with the data structure? Absolutely, a bug can lead to creating an exception (data structure). Consider:
But that wouldn't be an exception (concept), that'd just be a bug. The fault is in the inverted conditional, not a fundamental execution flaw.color sky = "blue"; if (sky == "blue") { // was intended to be sky != "blue" throw "the sky was expected to be blue"; // produces an exception data structure (message, call stack, etc.) }The presence of an exception (data structure) does not imply that there is an exception (concept). The overlapping use of words is a bit unfortunate, perhaps — albeit consistent with error (concept) and error (data structure), but the difference between a data structure and a concept should not confuse anyone.
Exceptions (concept), on the other hand, looks more like:
The code is fundamentally flawed. The machine is, in this case, quite able to determine that you screwed up. If you have a sufficiently advanced compiler, you'd be warned at compile time that methodThatDoesNotExist cannot be called. But if that condition evades the compiler then it would lead to an exceptional event at run time instead.object.methodThatDoesNotExist();- First, your categories are not universal. They depend on the language and the runtime. 1. Divide by zero In IEEE floating point you can define it to yield Infinity or NaN and keep going. In some languages it raises an exception. In C with integers you get undefined behavior with no exception at all. If your claim were about a fundamental flaw of code itself, the result would not change across environments. It does. So your category is a policy choice, not a law of nature. 2. Out of bounds In Java you get an IndexOutOfBoundsException. In C you can trample memory and never raise anything. Same code pattern, different outcome. Again this shows your supposed bright line is just a design decision. 3. methodThatDoesNotExist In a statically typed language this is a compile time error and never throws at runtime. In a dynamic language it can throw NoSuchMethod at runtime. Same text, different category, based on tooling. That is not a fundamental metaphysics of exceptions. It is the compiler and runtime drawing the line.
Conclusion from 1 through 3 Your definition of exception as a fundamental flaw collapses the moment we cross a language boundary. What you are calling fundamental is actually configurable convention.
⸻
Second, your machine versus human distinction is self defeating.
You say the machine can determine exceptions but only humans can determine bugs. The machine does not determine anything. It executes rules we wrote. If a rule maps a state to raise an exception, the machine raises one. Whether that state is a bug is defined by the spec. If the spec says division by zero must never occur in production, any runtime that hits it is a bug. If the spec says handle it as Infinity, then hitting it is not a bug. The label follows intent and contract, not the instruction pointer.
Obvious example A payment service has a rule that user input must be validated before division. A request slips through with divisor zero and the service throws. That is a bug even if the language designer felt very proud of raising an ArithmeticException. Flip the rule. If the service is specified to accept zero and return a sentinel value, then raising an exception is itself the bug.
⸻
Third, the data structure versus concept move does not rescue the argument.
Yes, we all know you can throw an object on purpose. Yes, we all know many libraries create rich exception objects. None of that changes the core point. In a nontrivial system, unhandled exceptions or exceptions raised in states that the spec forbids are bugs by definition. The exception is the transport. The bug is the violation that made the transport fire.
Your own toy example proves it You show an inverted conditional that throws. You say that is not an exception in the conceptual sense. But the system would still page the on call, still fail the request, still violate the acceptance test. Every operator and every customer would call that a bug. You are trying to win by relabeling the alarm as not part of the fire drill.
⸻
Fourth, here are obvious scenarios that make the logic clear to anyone.
Elevator Spec: doors must never open between floors. Code hits that state and trips a safety exception. If that ever happens in production, it is a bug. If the safety system decides to log and keep moving instead, and the spec requires a stop, then not throwing is the bug. The exception path is just the messenger.
Bank transfers Spec: never double charge. Off by one posts the same transfer twice. The code throws on the second insert due to a unique constraint. If it throws, the incident is a bug. If it does not throw because the database had no constraint and you silently post two charges, that outcome is a bug. In both cases the presence or absence of an exception is not the category. The spec is the category. The bug is the deviation.
Medical device Spec: sensor readings out of range must be clamped and logged, not crash the loop. If the runtime raises an exception and the loop dies, that exception is the manifestation of a bug. If the runtime never raises and you keep using garbage without logging, that silent path is the bug. Same state space. Different handling. The spec decides which path is the failure.
Toaster Spec: pop after two minutes. The timer arithmetic overflows and you get a runtime exception in the control loop. That exception is how the bug announced itself. In a different implementation the overflow wraps and the toaster never pops. No exception at all. Still a bug.
These are obvious because everyone can see that what matters is the contract. The exception is only a vehicle for discovery.
⸻
Fifth, the correct taxonomy already exists and it does not match your story.
Industry standards separate three things. A fault is the cause in the code or design. An error is the incorrect internal state. A failure is the externally visible deviation. Exceptions are one mechanism for surfacing an error or failure. When an exception causes a failure relative to the spec, we have a bug. When an exception is expected and fully handled within the spec, we do not. This mapping is stable across languages in a way your concept versus data structure split is not.
⸻
Final point
Your thesis needs both of these to be true at once.
A) Exceptions mark fundamental flaws independent of intent. B) Bugs are only deviations from intent.
But we have shown counterexamples where intent flips the classification and where language choice flips the behavior. That means A and B cannot both stand. Once A falls, your whole distinction reduces to wordplay. Once B stands, the only consistent rule is this:
If an exception leads the program to violate its requirements, it is evidence of a bug. If it does not, it is routine control flow. In practice many exceptions are how bugs announce themselves. That is why production teams triage exception rates and not treatises on metaphysics.
- > What you are calling fundamental is actually configurable convention.
Not within the topic's layer of abstraction. I can see you put a lot of thought into this, and if you were writing in a vacuum it might even be interesting, but discussion relies on context...
> If that ever happens in production, it is a bug.
Right. A bug, not an exception. Overloading the use of exception data structures and associated control flows does not imply that there is an exception in concept, just like using an error data structure to represent an email address[1] does not imply that you are dealing with an error in concept. It's just an email address.
> If it throws, the incident is a bug.
Right. A bug, not an exception. Again, just because you overloaded the use of a feature intended for dealing with exceptions for other purposes does not mean that you have an exception in concept. Just as an email address does not become a conceptual error just because you put it in an error data structure[1].
> If the runtime raises an exception and the loop dies, that exception is the manifestation of a bug.
Right. A bug, not an exception. I sense a recurring theme here. It is clear now that you've undeniably confused exceptions (data structure) with exceptions (concept). You obviously missed a lot of context so this may come as a surprise, but we were talking about the concept of programmer error. Data structures used in an implementation find no relevance here.
> In practice many exceptions are how bugs announce themselves.
As bugs and exceptions are different categories within the broader concept of programmer error, you could equally say "In practice many bugs are how bugs announce themselves". How, exactly, am I to grok that? I honestly have no idea what to make of that statement.
[1] let email = Error("foo@example.com")
- >Not within the topic’s layer of abstraction. I can see you put a lot of thought into this, and if you were writing in a vacuum it might even be interesting, but discussion relies on context…
You keep invoking “context” as a shield for avoiding substance. Context doesn’t change semantics. Arithmetic faults, null dereferences, and contract violations are not “configurable conventions” no matter how much pseudo-philosophical varnish you apply. Calling fundamental runtime behavior a “configurable convention” is like claiming gravity is optional if you talk about it at the right “layer of abstraction.” It sounds deep until anyone with an actual engineering background reads it.
⸻
>Right. A bug, not an exception. Overloading the use of exception data structures and associated control flows does not imply that there is an exception in concept, just like using an error data structure to represent an email address[1] does not imply that you are dealing with an error in concept. It’s just an email address.
Your “email analogy” is nonsense. The fact that something can be misused doesn’t invalidate its conceptual role. By that logic, if someone writes int banana = "hello", integers stop existing. Exceptions are not defined by their container type. They are defined by the runtime condition they represent. The fact that you have to keep insisting otherwise shows you have no grasp of the operational meaning of exceptions beyond the syntax of throw.
⸻
>Right. A bug, not an exception. Again, just because you overloaded the use of a feature intended for dealing with exceptions for other purposes does not mean that you have an exception in concept. Just as an email address does not become a conceptual error just because you put it in an error data structure[1].
Repeating the same broken analogy doesn’t make it coherent. You keep hammering at this contrived misuse like it proves anything. It doesn’t. It’s a straw man. Nobody is arguing that misusing the mechanism redefines the concept. You’ve constructed a toy example where you intentionally misuse a type and then triumphantly point to your own confusion as evidence. This is the intellectual equivalent of setting your keyboard on fire and declaring that typing is impossible.
⸻
>Right. A bug, not an exception. I sense a recurring theme here. It is clear now that you’ve undeniably confused exceptions (data structure) with exceptions (concept). You obviously missed a lot of context so this may come as a surprise, but we were talking about the concept of programmer error. Data structures used in an implementation find no relevance here.
You’re projecting your own confusion. You can’t even decide which layer you’re talking about. One moment you say exceptions are “data structures,” the next you say the data structure is irrelevant. You wave your hands at “concepts” but those concepts are precisely instantiated through those data structures. You don’t get to discard the implementation layer when it undermines your argument. Pretending the runtime’s behavior is irrelevant to the “concept of programmer error” is a convenient way to avoid admitting that bugs and exceptions are related manifestations of the same fault system. You’re not clarifying categories; you’re just renaming them until they stop overlapping.
⸻
>As bugs and exceptions are different categories within the broader concept of programmer error, you could equally say “In practice many bugs are how bugs announce themselves”. How, exactly, am I to grok that? I honestly have no idea what to make of that statement.
Of course you don’t. The problem isn’t the statement; it’s your refusal to recognize that categories can overlap without collapsing. Saying exceptions often announce bugs is perfectly coherent: bugs are latent causes, exceptions are how those causes surface at runtime. Your confusion stems from trying to turn mutually informative categories into mutually exclusive ones. It’s the same error a freshman makes when they insist that “rain isn’t weather because weather causes rain.”
⸻
[1] The Error("foo@example.com") example doesn’t reveal deep insight; it reveals that you can misuse syntax. That’s not philosophy, it’s just bad code.
- I'm currently working on something that requires a GPU with CUDA at runtime. If something went wrong while initializing the GPU, then that'd be an exceptuion/bug/"programming error" most likely. If the user somehow ended up sending data to the GPU that isn't compatible/couldn't be converted or whatever, then that'd be an user error, they could probably fix that themselves.
But then for example if there is no GPU at all on the system, it's neither a "programming error" nor something the user could really do something about, but it is exceptional, and requires us to stop and not continue.
- > If something went wrong while initializing the GPU, then that'd be an exceptuion/bug/"programming error" most likely.
That depends if it is due to the programmer making a mistake in the code or an environmental condition (e.g. failing hardware). The former is exceptional if detected, a bug if not detected (i.e. the program errantly carries on as if nothing happened, much the dismay of the user), while the latter is a regular error.
> But then for example if there is no GPU at all on the system, it's neither a "programming error" nor something the user could really do something about, but it is exceptional
Not having a GPU isn't exceptional in any sense of the word. It is very much an expected condition. Normally the programmer will probe the system to detect if there is one and if there isn't, fall back to some other option (e.g. CPU processing or, at very least, gracefully exiting with feedback on how to resolve).
The programmer failing to do that is exceptional, though. Exceptions are "runtime compiler errors". A theoretical compiler could detect that you forgot to check for the presence of a GPU before your program is ever run.
The grey area is malfunctioning CPU/memory. That isn't programmer error, but we also don't have a good way to think about it as a regular error either. This is what "bug" was originally intended to refer to, but that usage moved on long ago and there is seemingly no replacement.
- That’s interesting. I’d actually consider this a user error because it’s only in the user’s power to fix it.
For example:
1. You’d want to display a message that they need a GPU.
2. Call stack information isn’t helpful in diagnosing the issue.
- Not all exceptional circumstances are bugs
- That's subtly different. It's secondary whose fault is this, what primarily matters is whether you should continue with the rest of the process.
There is always a cleanup layer, the trick is to choose well between 1 and 2:
Take for example an unexpected divide by zero, that's a classic invariant violation. The entire process should blow up right there because:1. Some code in the same OS process is able to bring data back to order. 2. OS can kill the process and thus kill any corruption that was in its address space. 3. Hardware on/off button can kill the entire RAM content and thus kill any corruption that spilled over it.- it knows that the data in memory is currently corrupted,
- it has no code to gently handle the corruption,
- and it knows the worst scenario that can happen: some "graceful stop", etc., routine might decide to save the corrupted data to disk/database/third-party. Unrecoverable panic (uncatchable exception) is a very good generic idea, because persistently-corrupted-data bug is a hundred times worse than any died-with-ugly-message bug as far as users are concerned.
- > Exceptions should be reserved for developer errors like edge cases that haven’t been considered or invalid bounds which mistakenly went unchecked.
Isn't this what assertions are for? How would a user even know what exceptions they are supposed to catch?
IMO exceptions are for errors that the caller can handle in a meaningful way. Random programmer errors are not that.
In practice, exceptions are not very different from Result types, they are just a different style of programming. For example, C++ got std::expected because many people either can't or don't want to use exceptions; the use case, however, is pretty much the same.
- I’ve often seen assertions throw exceptions when violated. Users don’t catch exceptions, developers do. Users interact with the software through things like failure pop ups. You’d need to check that there’s a failure to show one, hence returning a Result to represent the success/fail state.
- With users I meant library users, not end users.
> You’d need to check that there’s a failure to show one
You can do this with either exceptions or Result types. At the end of the day, it is a matter of culture and personal preference.
There appears to be some useless code there. Why is validRequest.email being passed to throwIfExists twice? Why is throwIfExists implemented to return the email if the following line (runWithSafety) just ignores that returned email and instead gets the email from registrationRequest.email?fun register(registrationRequest: UserRegistrationRequest): UserDTO { return success(registrationRequest) .flatMap { validRequest -> throwIfExists(validRequest.email) { authService.userExists(validRequest.email) } }.flatMap { runWithSafety { authService.register(registrationRequest.email, registrationRequest.password) } }.getOrThrow() }- And won’t the authService.register function also error if the user already exists? Or will it allow double registering the account?
There are deeper problems here that a Result type is not gonna fix.
- Not a fan. Code branches and this is Good Thing(TM). Result violates the single responsibility principle and tries to make what are distinct paths into a single thing. If your language has exceptions and returned values as distinct constructs then obfuscating them with Result means you end up fighting the language which becomes apparent fairly quickly. It's also a frustrating experience to want to interact with returned values directly and constantly have to deal with a polymorphic wrapper.
- I don't see how try-catch promotes single responsibility principle. I feel like this principle is just arbitrary.
If I have Result<Error, Value> and I change the Error, I have to change all places that are using the Error type and tweak the error handling in mapLeft or flatMapLeft.
If I instead raise Error and change it, I have to look at all the places where this error explodes and deal with it, not to mention, most languages won't even give me a compile time warning if I still keep the previous error type.
I agree that if language does not have do-notation, that it's a bit ugly to sprinkle map and flatMap everywhere. Good example of ugliness is https://github.com/repeale/fp-go
I think only an effect system, or a big environment object, places everything at 1 place, and when types change you have 1 place to edit the code. But starting immediately with an effect system (to abstract away control flow) or big env (to lift all ifs up) is premature.
You're trying to equate code paths (outcome handling) with values (outcomes), and that seems a more unfortunate category error.Result violates the single responsibility principle and tries to make what are distinct paths into a single thing.- There are cases when you need to store the result of a function regardless of if it succeeded or threw. Like if you need to tell the user that an error occurred. In those situations, a `Result` can make a lot of sense.
Arguably, `Result` can also help when it is important that error are dealt with and not just allowed to bubble up.
- I'm also not a fan. Due to your point regarding code branches, but also because I just don't find the code very readable.
I think Result<T> has its use, but I don't think this is a particular great example.
- > Result violates the single responsibility principle and tries to make what are distinct paths into a single thing.
Only in languages that struggle to represent multiple shapes of values with a single type. I don't think I ever want to use a language with exceptions again.
- What language do you use which does not have exceptions?
- C, Rust, Go. With a little effort, Scala can be used this way quite naturally, simply by allowing exceptions to crash the process.
- How do you handle situations where crashing the process is inappropriate. I may not care that a library doesn't like its log file or network socket disappearing, but I very much care if its exception about that event kills the entire process.
- [dead]
- Since this looks like Kotlin, worth pointing out that there is a kotlin class in the standard library called Result. I've been using that for a few things. One place that I'm on the fence about but that seems to work well for us is using this in API clients.
We have a pretty standard Spring Boot server with the usual reactive kotlin suspend controllers. Our api client is different. We were early adopters of kotlin-js on our frontend. Not something I necessarily recommend but through circumstances it was the right choice for us and it has worked well for us in the last five years. But it was a rough ride especially the first three of those.
As a consequence, our API client is multiplatform. For every API endpoint, there's a suspend function in the client library. And it returns a Result<T> where T is the deserialized object (via kotlinx serialization, which is multiplatform).
On the client side, consuming a result object is similar to dealing with promises. It even has a fold function that takes a success and error block. Basically failures fall into three groups: 1) failures (any 4xx code) that probably indicate client side bugs related to validation or things that at least need to be handled (show a message to the user), 2) internal server errors (500) that need to be fixed on the server, and 3) intermittent failures (e.g. 502, 503) which usually means: wait, try again, and hope the problem goes away.
What I like about Result is making the error handling explicit. But it feels a bit weird to client side construct an Exception only to stuff it into a Result.error(...) instead of actually throwing it. IMHO there's a bit of language friction there. I also haven't seen too many public APIs that use Result. But that being said, our multiplatform client works well for our use.
But I can't expose it to Javascript in its current form; which is something I have been considering to do. This is possible with special annotations and would mean our multiplatform client would be usable in normal react/typescript projects and something I could push as an npm. But the fact my functions return a Result makes that a bit awkward. Which is why I'm on the fence about using it a lot.
So, nice as a Kotlin API but good to be aware of portability limitations like that. You would have similar issues exposing Kotlin code like that to Java.
- > But it feels a bit weird to client side construct an Exception only to stuff it into a Result.error(...) instead of actually throwing it
yep, `kotlin.Result` constraining your error type to `Throwable` is a real headache as it forces you to still model your domain logic via exceptions. it also means people can still accidentally throw these exceptions. not to mention the overhead of creating stack traces per instantiation unless you disable that on every subclass.
i recommend using https://github.com/michaelbull/kotlin-result?tab=readme-ov-f... (which has a nice breakdown of all the other reasons to avoid `kotlin.Result`)
- ArrowKt is also worth a mention: https://arrow-kt.io/learn/typed-errors/
- Thank you, a very insightful comment :) As a side note, my latest post (on the same website) is on "reactive" Java / suspend functions in Kotlin.
Probably the most harmful snippet ever written. Every blog post about errors has something similar written, regardless of the programming language. Please, don't write, suggest or even pretend that this should exist.} catch (exception: Exception) { // log exception throw exception }- Whats the problem ? Is throwing directly better here ? Given the annotations I presume this is spring so it will probably be logged anyway. So maybe its unnecessary here.
- I am not a fan of function chaining in the style advocated in the article. In my experience functional abstractions always add function call indirection (that may or may not be optimized by the compiler).
You don't need a library implementation of fold (which can be used to implement map/flatmap/etc). Instead, it can be inlined as a tail recursive function (trf). This is better, in my opinion, because there is no function call indirection and the trf will have a name which is more clear than fold, reducing the need for inline comments or inference on the part of the programmer.
I also am not a fan of a globally shared Result class. Ideally, a language has lightweight support for defining sum/union types and pattern matching on them. With Result, you are limited to one happy path and one error path. For many problems, there are multiple successful outputs or multiple failure modes and using Result forces unnecessary nesting which bloats both the code for unpacking and the runtime objects.
- Functional abstractions are great for writing code. They allow to nicely and concisely express ideas that otherwise take a lot of boilerplate. Now, for trying to debug said code... gl hf.
- There's certainly situations where this pattern creates some tricky ambiguity, but more often than not Result is quite an ergonomic pattern to work with.
In case it's useful for anyone, here is a simple plug-in-play TypeScript version:
```
type Ok<T = void> = T extends undefined ? { ok: true; } : { ok: true; val: T; };
type Err<E extends ResultError = ResultError> = { ok: false; err: E; };
type Result<T = void, E = ResultError> = { ok: true; val: T; } | { ok: false; err: E | ResultError; };
class ResultError extends Error { override name = "ResultError" as const; context?: unknown; constructor (message: string, context?: unknown) { super(message); this.context = context; } }
const ok = <T = void>(val?: T): Ok<T> => ({ ok: true, val: val, } as Ok<T>);
const err = (errType: string, context: unknown = {}): Err<ResultError> => ({ err: new ResultError(errType, context), ok: false, });
```
```
const actionTaker = await op().then(ok).catch(err);
if (result.ok) // handle error
else // use result
```
I will be forever grateful to the developer first introduced to this pattern!
- This doesn't pass the smell test. Whenever I've seen the Result or Either type, the definition looked different than what you wrote here. I doubt this composes nicely, with Folktale and fp-ts I can be certain.
- I've been toting around and refining a Result<> implementation from project to project from Java 8 onwards. Sealed classes in 17+ really make it shine.
I wish Oracle et al. had the courage to foist this into the standard library, damn the consequences. Whatever unanticipated problems it would (inevitably) create are greatly outweighed by the benefits.
I've written Pair<> about a dozen times as well.
- Good article.
Maybe you could look up the Try monad API (Scala or Vavr works in Java + Kotlin), by using some extra helper methods you can have something probably a little bit lighter to use.
I believe your example would look like the following with the Try monad (in Java):
The login() function would be using the same pattern to call authService.verify() then filtering nonNull and signing the JWT, so it would be the same pattern for both.public UserDTO register(UserRegistrationRequest registrationRequest) { return Try.of(() -> authService.userExists(registrationRequest.email)) .filter(Objects::isNull, () -> badRequest("user already exists")) .map(userId -> authService.register(registrationRequest.email, registrationRequest.password)) .get(); } - For the typescript world, there is neverthrow[0] which offers a similar Result type.
everywhere orPromise<Result<number, string>>
and thentype EitherT[F[_], E, A] = F[Either[E, A]]
?def dosomething(): F[String, Number]Isn't this beautiful: https://github.com/7mind/distage-example/blob/develop/bifunc... ?
- Why does it have Either? Doesn't TypeScript have "A | B" style sum types?
- Either is biased, union is not.
Probably we should say "union" instead of sum, as typescript unions are not discriminated. string | string in typescript is exactly the same as just string, while Either[String, String] is a type which is exactly a sum of two string types. Plus Either is biased towards R, the happy path value.
- [dead]
- Smells like something that Effect-TS is designed to solve in the TypeScript world.
- It is always funny to see that we try and force formulas to the early elementary shape that people learn. Despite the fact that chemistry, biology, physics, etc. all have advanced shapes for equations that do not have the same concerns.
Similarly, when constructing physical things, it is not uncommon to have something with fewer inputs than outputs. Along with mode configured transfer of input to outputs.
- Result<T> is a built-in in kotlin, but this enforces that the error type is a Throwable
If you fancy that an error could be just a type, not necessarily a Throwable, you might like Result4k - it offers a Result<T,E>
https://github.com/fork-handles/forkhandles/tree/trunk/resul...
disclaimer: I contribute to this.
- Nice, what's the KMP plan there?
We currently use https://github.com/michaelbull/kotlin-result , which officially should work on KMP, but has some issues.
- In Kotlin, my go-to library is https://arrow-kt.io/
- With regard to AI, why not throw this whole article in an .md file and point CLAUDE.md to it? Codex is better at following rules so maybe you’d have more luck with that. But yeah, AI won’t code your way by default. People expect way too much out of the interns, they need direction.
- This is one of the issues with LLMs in dev IMO.
You either have the case that tech moves on and the LLM is out of date on anything new, so adoption slows or you have tech slowing down because it doesn't work with LLMs so innovation slows.
Either way, it's great if you're working on legacy in known technologies, but anything new and you have issues.
Can I write a spec or doc or add some context MCP? Sure, but these are bandaids.
- The imperative code has
which doesn't exist in the Result version.// log exception - With the ? syntax in Rust results and exceptions are the same thing. I posit that the former is superior. It is unfortunate that results have worse performance but I don't see any reason why. Results that bubbles up all the way ought to be identical to an uncaught exception.
- Exceptions can tradeoff happy-path performance for more overhead on the slow path. For example, an exception implementation can make it so that callers can assume the 'Ok' result always appears, because an exception causes a seperate unwinding mechanism to occur that walks the stack back, bypassing that entirely. In contrast every caller to a function that returns a Result must have a branch on that result, and this repeats for each part of the callstack.
This also means that exceptions can have stacktraces that only incur a cost on the unhappy path and even only if that exception is uncaught. While if you want a trace for a bad Result you are going to be doing a lot of extra book-keeping that will be thrown away
In general I agree that Results are the better abstraction, but there are sadly some tradeoffs that seem to be hard to overcome.
- This depends a lot of what you are using exceptions for. I think in general the branch on Ok/Err is probably not meaningful performance-wise because the branch predictor will see right through that.
But more generally the happy-path/error-path distinction can be a bit murky. From my days writing Java back in the day it was very common to see code where checked exceptions were used as a sort of control flow mechanism, so you end up using the slow path relatively frequently because it was just how you handled certain expected conditions that were arbitrarily designated as "exceptions". The idea behind Result types to me is just that recoverable, expected errors are part of the program's control flow and should be handled through normal code and not some side-channel. Exceptions/panics should be used only for actually exceptional conditions (programming errors which break some expected invariant of the system) and immediately terminate the unit of work that experienced the exception.
- gosh...
no, no, no!try { val user = authService.register(registrationRequest.email, registrationRequest.password) return user } catch (exception: Exception) { // log exception throw exception }the whole point of the exceptions (and moreso of the unchecked ones) is to be transparent!
if you don't know what to do with an exception do NOT try to handle it
that snippet should just be
return authService.register(registrationRequest.email, registrationRequest.password)- I'm gonna plug my favorite note on this topic: https://ericlippert.com/2008/09/10/vexing-exceptions/
Both snippets suffer from being too limited. The first, as you point out, catches too many exceptions. But the second.... What happens if the email address is taken? That's hardly exceptional, but it's an exception that the caller has to handle. Your natural response might be to check if the email address is taken before calling register, but that's just a race condition now. So you really need a result-returning function, or to catch some (but probably not all) of the possible exceptions from the method.
- The way I usually structure it is that the only exception would be some type of failure to connect. Any actual error thrown by the service comes back as a result.error, and any failure (like email address taken) comes back as result.fail. This way you can separate it into (1) connection problem, (2) backend error/bug/database offline/etc, (3) both of those are fine but the backend doesn't like the input.
- I agree, this was just a sample code to show how usually imperative if / else / try / catch code is written. What is also possible is we catch the exception, log it and throw another one.
- I have had to write a Result abstraction multiple times. Honestly I think it should be part of standard java much like Optional.
- Stick your services into the type too, and you have `Effect`[0], the ultimate monad :)
- Result is great but it ideally needs extensible union types (polymorphic variants) plus exhaustive pattern matching to work well.
- > At the first glance, this code looks noisier and hard to understand
Because of your inconsistent line-breaks!
- This is why Haxe is awesome. You can target a sloppy langauge and still get the benefits os a ML-like typesystem.
- Ah, Either. Didn't recognize you from the first glance.
Now we need to invent do-notation, higher kinds and typeclasses and this code would be well composable.
- I love how everyone here shares real experience with Kotlin and Result, it’s cool to see different views that actually teach something.
- Use the builtin Result class and runCatching/fold and be done with it. Yes, it has shortcomings but works well enough in practice.
- How to rewrite boring, easily understood code into abomination. I'm not surprised to see Kotlin, for some reason there's a huge inferiority complex in Kotlin community where you have to write the most convoluted pseudo-fp code possible (not smart enough to use ML or Haskell, but still want to flex on Java noobs).
I can't wait until they release rich errors and this nonsense with reinventing checked exceptions will finally end.
- This is really just a syntactical issue. Not one of types or semantics.
Non trivial operations have errors when the happy path fails. And with web apps IO can fail anytime, anywhere for any reasons.
Sometimes you want to handle them locally, sometimes globally. The question is how ergonomic it is to handle this all for a variety of use cases.
We keep reinventing the wheel because we insist that our own use cases are “special” and “unique”, but they really aren’t.
Personally, I think Java’s proposal on catching errors in switches, next to ordinary data is the right step forward.
Monads are great. You can do lots of great things in them, but ergonomic they are not. We should avoid polluting our type systems where possible.
- Ah yes, -2. Predictable result on this emotional topic.