Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Exception based error handling is so bad and unsafe that adopting functional error handling with Either, Try etc as implemented by functional addon libraries for many languages, while not yet common, in time it will become the new default even in OO languages. (just like it's been the default in functional languages for decades)

Functional error handling types are much simpler, safer and more powerful.

Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.

Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)

Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.

> What is wrong about dedicated syntax

It adds complexity to the language! It could be that, when learning Java, Kotlin and any other language, we learn that methods return what they say they do... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.

> Exceptions are easier

But that's the point, they're not.

Exceptions based error handling is unnecessary, hugely complex, doesn't compose at all, obfuscates or straight up hides what can go wrong with any given call, so leads to countless trivially preventable bugs... I could go on. And after decades of use, there's still no consensus about what exceptions should be or how they should be used. Exceptions are a failed experiment and I have no doubt that in ten years, Java, Kotlin and many other languages will acknowledge as much and move away from it the same way Joda Time outcompeted and replaced the horrible Java date and time library.



Checked exceptions are exactly analogous of Result/Either types. They are just built into the language with syntactic sugar, automatically unwrap by default (the most common operation), can be handled on as narrow or wide scope as needed (try-catch blocks), does the correct thing by default (bubbling up), and stores stack traces!

In my book, if anything, they are much much better! Unfortunately they don’t have a flawless implementation, but hopefully languages with first-class effects will change that.


> Checked exceptions are exactly analogous of Result/Either types.

No, they aren't.

They are not compositional. You may want to write `f(g())` but there's no way to write the parameter type of `f` to make this work (in Java). That's because checked exceptions are an "effect" that would require extending the Java type system.


How would you define f in a different language such that f(g()) worked? You couldn’t do that in Go, for instance.


There's at least one language, Koka (https://koka-lang.github.io/koka/doc/book.html) that has effects in its type system. There are probably others. If you don't have effects, you could use union types.


This language looks really cool. Thanks for sharing.


In rust, you write "f(g()?)", and make sure that your function body returns a Result<> with an error type that g()'s error type can be converted to.

It works great. Also, note that f() doesn't care about the type of error returned by g(), and that it will be a compilation error if g()'s error type turns into something incompatible.

Sadly, there are proposals to add exceptions to rust, and it seems likely they will be accepted, breaking error handling semantics across the entire ecosystem (even in new code, since exceptions are too hard to use correctly).


What's the rationale behind adding exceptions?


I'm not sure what they mean by adding exceptions.

AFAIK, the only proposal related to exceptions is adding a `try` block that would scope the `?` operator to that block instead of the current function.


> You couldn’t do that in Go, for instance.

You can – by making f accept g's return types, including the error. This is even being done in the Go standard library: https://pkg.go.dev/text/template#Must


That’s interesting, but consider two functions like func ParseInt(string) (int, error) and func Abs(int) int. You lose some composability when using errors over exceptions. The Rust solution mentioned elsewhere seems elegant.


Simple! You just make f take in g's result type.


... not sure if serious or not



I'm completely serious in the sense that you could do that and really would in some situations.

You might do it because you want to factor the handling of the different result cases out to another function.


You don't think it would limit the independent use of f()?


No? Surely if f() takes a full result (including error condition) it's because there's something it wants to do with that?


You could actually, iirc if f takes as many parameters as g returns it works out.

In a better langage f would take a Result, and then it can manipulate that however it wants.

Obviously you can also plug in adapters if you need some other composition e.g. g().map(f), g().and_then(f), …


Read my last paragraph.


So they are better... in a theoretical implementation.


A prototype of the Java compiler with composable checked exceptions existed at one point, so we know it's definitely doable.


To my mind the big issue with existing examples of checked exceptions is that the language to talk about the exceptions is woefully inadequate, so it stops you from writing things that would be useful while dealing correctly with exceptions. The go-to example is a map function, which we should be able to declare as throwing anything that might be thrown by its argument. Without that we need to either say that map might throw anything and then handle cases that actually can't happen, suppress/collect exceptions inside map, or suppress/collect errors inside the functions we're passing to map, all of which add boilerplate and some of which add imprecision or incorrectness. It would also be good to be able to state that a function handles some exceptions if they are thrown by its argument. And all of this should be able to be composed arbitrarily. And... somehow not be too complicated. For usability, it should probably also be possible to infer what's thrown for functions that are not part of an external API.


No, unfortunately they are not. The problem is not with checked exceptions themselves, but with the other type of exceptions in Java.

In languages that rely on Result/Either for error handling, you've got two types of errors: Typed errors (Result/Either) and untyped panics. Typed errors are supposed to be handled, possibly based on their type, while panics can be recovered from ("catched") but these are serious, unexpected errors and you're not supposed to try to handle them based on their type. Since typed errors generally need to be handled explicitly while untyped errors are unexpected, typed errors are always checked (you can't skip handling them), while untyped errors are unchecked (implicitly propagated up the stack if you don't do anything to catch them).

Java has three types of errors:

1. Checked errors, a.k.a. checked exceptions: (exceptions that inherit from Exception, but not from RuntimeException). 2. Unchecked application errors: exceptions that inherit from RuntimeException. 3. Unchecked fatal errors: exceptions that inherit from Error.

These three kinds of errors live in a confusing class hierarchy, with Throwable covering all of them and unchecked application errors being a special case of checked application errors.

Like everything else designed in the early Java days, it shows an unhealthy obsession with deep class hierarchies (and gratuitous mutability, check out initCause()!). And this is what destroyed the utility of checked exceptions in Java in my opinion.

Consider the following example: We have a purchase() function which can return one of the following errors:

- InsufficientAccountBalance - InvalidPaymentMethod - TransactionBlocked - ServerError - etc.

You want to handle InsufficientAccountBalance by automatically topping up the user's balance if they have auto top-up configured, so you're going to have to catch this error, while letting the rest of the errors propagate up the stack, so an error message could be displayed to the user.

In Rust, you would do something like this:

  account.purchase(request).map_err(|err| match err {
    PurchaseError.InsufficientAccountBalance(available, required) => {
      account.auto_top_up(required - available)?
      account.purchase(request)
    }
    _ => err // Do not handle other error, just let them propagate
  })
In Java, you would generally do the following:

  try {
    account.purchase(request);
  } catch (InsufficientAccountBalance e) {
    account.auto_top_up(e.requiredAmount - e.availableAmount);
    account.purchase(request);
  } catch (Exception e) {
    // We need to catch and wrap all other checked exception types here
    // or the compiler would fail
    throw new WrappedPurchaseException(e);
  }
The "catch (Exception e)" clause doesn't just catch checked exceptions now - it catches every type of exception, and it has to wrap it in another type! Of course, you can also specify every kind of checked exception explicitly, but this is way too tedious and what you get in practice is that most code will just catch a generic Exception (or worse - Throwable!) and wrap that exception or handle it the same way, regardless if it was a NullPointerException caused by a bug in code, an invalid credit card number.

The worst problem of all is that once developers get used to write "catch (Exception e)" everywhere, they start doubting the values of checked exceptions: after all, most of their try clauses seem to have a generic "catch (Exception e)", so does it really matter at all of they're using checked exceptions?

This is the reality. Checked exceptions failed in Java. Most Java developers see them as nothing more than a nuisance and look for ways to bypass them. That does not necessarily mean that the concept of checked exception as a language level facility for errors has failed, but it certainly failed the way it has been implemented in Java.


It's the ergonomics of it. For a checked exception facility to not be maddening, it needs to have good facilities to propagate and wrap exceptions easily and with minimal scaffolding. But for some reason no mainstream language with traditional exception handling did that.

In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.


> In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.

I wholly agree with this sentiment. Rust Result types where also excruciatingly inconvenient to use at the early days, but Rust gradually added facilities to make it better: first the try! macro and if-let, then the ? operator and finally let else. Together with crates like anyhow, thiserror and eyre, error handling became a lot better. I don't use Swift a lot, but it also seem to have iterated on its error handling.

In the 27 years of its existence, Java did very little to improve exception handling facilities. It added exception chaining in Java 1.4 and catching multiple exceptions in Java 7, that's it. I'm not picking up specifically on Java here - I think many languages neglect exceptions or error handling. Go is also an instructive example of a language that chose a non-exception-based error handling mechanism that the designers claimed to be superior, but failed to add ergonomics to that. This is not for the lack of trying though: the Go team tried to fix this issue multiple times, but there are very vocal parts of the Go community who opposed any kind of ergonomics, in favor of "explicitness" (as if explicitness means "error-prone boilerplate"). I would give the Go team full score for seriously trying.

I give them less score on the composition-over-inheritance part though. Go is one of the languages that has objects (structs) and interfaces, but disallows inheritance, but it doesn't provide any mechanism for automating delegation. Kotlin has shown that this is possible and even quite simple. It's not one of these languages features (like method overloading, type classes and generics) that carries a lot of corner cases and complexity that you have to deal with.


If all of those exceptions are custom made, then it makes sense to have a common subclass for them. It is not ideal, I agree, but it is hardly a showstopper.


Exception based error handling is unsafe when they are unchecked exceptions. Checked exceptions however are as safe as Either, Try, Monads, Applicatives or whatever. You are forced to declare them in your method signature, the caller is forced to either handle them or rethrow them + declare them as well. And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions. This is why we see so many empty catch blocks or upcasting to Exception or even Throwable; it is laziness.

I would also argue that checked exceptions are no more complex than Eithers, Try or Applicatives. Actually passing Eithers or Applicatives around everywhere can easily clutter your code as well, IMO it can be worse than checked Exceptions.


> And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions. This is why we see so many empty catch blocks or upcasting to Exception or even Throwable; it is laziness.

Good developers should be lazy. Checked exceptions require you to do a bunch of cumbersome ceremony that makes your code unreadable, for what should be (and is, with Either or equivalent) at most a couple of symbols; no wonder developers hate that.

> I would also argue that checked exceptions are no more complex than Eithers, Try or Applicatives.

You'd be wrong. Checked exceptions as implemented in Java require two keywords of their own (throw and catch), a special change to method signatures (throws), a unique new kind of expression for multi-catch (|), a unique new kind of type for things caught in multi-catches, and special support in every other new language feature (e.g. Futures have a whole bunch of extra code to deal with exceptions). Eithers can literally be a regular class that you could write yourself using ordinary language features. A small piece of syntax sugar (something like "do notation", "for/yield comprehensions", or "?") is a good idea, but not essential, and if you do it right then you can implement that once across your whole language (for applicatives/monads in general) and use it for eithers, futures, database transactions, audit logs, almost anything. https://philipnilsson.github.io/Badness10k/escaping-hell-wit... .


Sorry but Either makes the code harder to read then exceptions. And harder to debug.


Only if you're ignoring errors. If you actually catch and handle them the exception-based code becomes harder to read.


Checked (and unchecked) exceptions create an exponential number of control flow paths. Consider:

   try {
      throws_a();
      r = grab_resource();
      throws_b();
      r.throws_a();
   } catch (a) {
      r.release(); // oops; null pointer some times
   } catch (b) {
      try {
        r.release();
      } catch (a) {
        // nooo....
      } 
   } finally {
     if r != null {
        r.finalize() // use after release (sometimes)
        r.release() // ???
     } 
   }
There is plenty of academic literature showing that real programs are even worse than my contrived example, on average.


The construction of arbitrary convolutions of logic is not demonstrative of your assertions. A multitude of code paths is not specific to checked exceptions. Note: It is possible to try catch a union of exception types in the recent versions of Java (catch X | Y | Z).

The paths are still there if the exceptions are all unchecked or combined or not using exceptions at all. Passing exceptions up is rarely the right choice, imo. If it's a terminal runtime, sure, obv.


> r.release(); // oops; null pointer some times

Quite sure Java would prevent that if r was never assigned.

Also most languages provide cleaner try-with-resources or RAII style lifetime management so you don't actually end up with that kind of spaghetti code unless you actively go out of your way to be bad at programming.


> Also most languages provide cleaner try-with-resources or RAII style lifetime management so you don't actually end up with that kind of spaghetti code unless you actively go out of your way to be bad at programming.

Context managers (try with resources) specifically don't work at all when release is conditional e.g. open a file, need some post processing, need to close the file if the post-processing fails but return it if it succeeds.

Defers don't either, unless you have a specialised one (e.g. zig's errdefer).

Actual RAII (aka affine types) does work, but adding that to a GC'd langage after the fact is a huge undertaking and increase in complexity, because the interaction between affine and normal types is fraught.


Ultimately, core language features that make things ergonomic enough for lazy developers to get it right will result in better software.

And ones that are so unergonomic that only industrious developers will get them right will result in worse software.

Modern languages allow these to be zero cost abstractions so there is little tradeoff.


I have been coding in Scala/ Haskell for the last 10 years, before that 15 years in Java. What I'm seeing is that there are as many lazy developers in FP as there are in Java, maybe even more. And despite all the nice safeguards that FP provides, there are still plenty of ways for lazy devs to work around them.

For instance the IO monad, which is used everywhere Scala/Cats. It can contain a result or an error. If you don't feel like checking for an error after you called some method, you can just pass it up and return the IO from your method. Does that sound familiar? It behaves just like a checked exception, the only difference is that methods don't need to declare any error or exception in their IO signature.


> It behaves just like a checked exception, the only difference is that methods don't need to declare any error or exception in their IO signature.

Then it behaves like an unchecked exception. Which is fine.

'Checking' is the act of modifying your source code to declare the error.


> And I guess this is precisely why so many developers hate them; they don't like the extra work they have to do to catch all those edge conditions

I hate checked exceptions when they force me to handle an exception which I know is impossible given the arguments passed to the method. For example, some older Java APIs take an encoding name and force you to handle the checked UnsupportedEncodingException - even when the encoding name was something like “US-ASCII” or “UTF-8” which is required to exist by the Java standard, and if somehow they didn’t there is often no possible error recovery than just crashing. This has been fixed in newer versions by introducing new APIs which throw an unchecked exception instead, and also by introducing constant objects for the standard charsets


> I hate checked exceptions when they force me to handle an exception which I know is impossible given the arguments passed to the method.

If I want to ignore a set of exceptions, I have the option to catch(Exception e) {} signaling that I recognize the risks that have been explicitly communicated by the API (that throws). An @IgnoreExceptions annotation would help dump the 5? boilerplate lines.

The unknown risks for other non-specific Runtime exceptions, are not included. I can catch those too if I add a catch(RuntimeException e){}, again signalizing that I recognize the risks such that other developers understand that I'm opting out of handling those conditions, which may or may not be errors in a classic sense. eg an expected socket not being available causing an IOException, because I'm doing some concurrent process.


I've seen checked exceptions cause numerous bugs. Why? Because they encourage you to put catch clauses everywhere, and a lot of developers don't know how to write them properly (and even the best developers sometimes make mistakes that slip through the cracks). A common result of this is that a bug causes an exception, but the catch clause loses information about the original exception (such as by logging it without the stack trace, or throwing a new exception without setting the cause). Or else the catch clause just ignores the exception, and then you get some other error later (e.g. NPE because some field was unexpectedly null), but again you've lost the info on what the root cause problem is. If your code base contains 100s of catch clauses, that's 100s of opportunities for buggy catch clauses to exist. And even if you eradicate them all from your own code base, then you might get hit by one buried in a third party library.

Without checked exceptions, you end up with far fewer catch clauses, and hence far fewer opportunities for buggy catch clauses. I think in most apps, most of the time, you just want to bubble all exceptions up to a central point which handles them – for example, in a web server, you can have a single catch clause for the whole HTTP request. If need be, you can make that central point extensible with custom handling with specific exceptions (like JAX-RS ExceptionMapper, or @ExceptionHandler beans in Spring – which you can then inject using IoC/DI). Once you've got that working, if you need catch clauses deeper in the code (e.g to return an error code to the frontend when the client sends bad data instead of just failing the request with a `NumberFormatException`), you can add them where appropriate – but unlike checked exceptions, you aren't forced to add them where you don't need them.


> I've seen checked exceptions cause numerous bugs.

I've seen quite a few in for-loops, so I'm not sure that's tracking for me.

> Why? Because they encourage you to put catch clauses everywhere

Java forces you to handle author-specified error conditions (CS error), but jr developers do tend to create their own more often than necessary.

While I agree that a global exception handler is good practice for Spring applications, individual exception handling is very common and important to modern software. eg If Web APIs get more complex (multiple datastores), you find you don't want to bubble everything up. I get a request, want to check a cache (which might throw) then look it up in a DB (which might throw) then look it up in a file (etc), then return a response.

I do wish I could handle exceptions simpler than making the choice to add 4 lines (+ any actual logging, etc) or blackhole-traveling through the stack to add checked exception signatures everywhere (code AND tests).


> I've seen quite a few in for-loops, so I'm not sure that's tracking for me.

The difference is that for-loops are almost an essential language feature – the vast majority of languages have them (or a close equivalent), and they make certain algorithms a lot easier to state clearly. Sure, there are some languages which lack them, but they tend to be either languages with non-mainstream paradigms (such as pure functional or logic programming languages) which put all the emphasis on recursion instead, or else really old legacy languages which predate the development of structured programming (such as pre-1980s versions of COBOL–modern COBOL versions have for loops)

By contrast, almost nobody considers checked exceptions an "essential language feature" – languages which lack them vastly outnumber languages which possess them, indeed, Java stands out as the only major mainstream language to possess them

Given the argument "this unusual inessential language feature causes more bugs than it prevents", the response "this essential language feature which the vast majority of languages have sometimes causes bugs too" isn't very convincing


> Checked exceptions however are as safe

No. If I add a checked UserNotFound exception to a getUser db call, you can bet someone higher up the stack will do try catch Exception e, so now they're catching OutOfMemory and who knows what else.


> you can bet someone higher up the stack will do try catch Exception e

But that's laziness on the caller's part. If I offer a method but the caller decides to do reckless lazy crap with it, there are many different ways to get there in any language. I typically call those out (Exception e) at code reviews.


This is the most common reply I see whenever anyone proposes a safer, better way of coding, and it's not a good one


...because doing the right thing should be convenient, for its own good.


As opposed to force unwrapping a Result type? Also, OutOfMemory is an error, exceptions won’t catch it.


Force-unwrapping a Result type is something you can do in multi-paradigm languages such as Rust, but not in stricter functional languages like Haskell - at least not easily (and we're worried about developers taking the easy way out here).

But more importantly, force-unwrapping is not equivalent to catching generic exceptions. Instead, it's equivalent to catching all checked exceptions and wrapping them in a Runtime error. It's also almost equivalent to what this compiler plugin does (or Kotlin or Lombok's @SneakyThrows do).

Catching "Exception" and trying to handle it generically, is more closely equivalent to this type of code:

  match result {
    Ok(value) => doSomethingWithValue(value)
    Err(e) => println("Error: {e}!")
  }


What about DivisionByZero, NumberFormatexception, ArrayStoreException?

There are countless examples of rare but legit non-obscure use-cases. And even if your code is fine, you can't expect the same for the libraries you are using.

And some exceptions make frequent non-happy paths more visible. Most of all IOException, because IO can _always_ fail for all the wrong reasons (because the failing of this exception is outside of the JVM's influence, it is rightfully a checked exception). And often you simply don't want to do the error handling at call-site but propagate to the code which is controlling the use-case.


Was that comment meant for me?


You can choose to unwrap the result type wherever you want, as opposed to it automatically unwrapping in place at the call site and having a weird propensity to encourage a "special" return from there.


OutOfMemoryError isn't an exception so they would not be catching it.


whatever, the point is exception allows and encourages people to catch less and more than they should


Sometimes top level code needs to do this. It's not always wrong. It's not always black and white in programming.


> Sometimes top level code needs to do this

come on, it's not as if anyone disagrees with that, but that's extremely, extremely rare, overwhelmingly, callers are just dealing things like with UserNotFoundException, NullPointerException, what have you, and there's no reason why the compiler should happily let you catch Exception (or nothing) when it could just be giving you an honest object back


It has not been extremely/overwhelmingly rare in my experience. Further still, within the context of Java, exceptions are objects.


Then that person should get a Java 101 class. Or is this the current “staff software engineer” level of skills?


This is the most common reply I see whenever anyone proposes a safer, better way of coding, and it's not a good one. "Just get better" like oh ok except that in the real world people are gonna people and even the best programmers in the world make mistakes and do dumb, lazy shit. Our tools should be designed such that the safest, most correct way to do anything has the path of least the resistance. Not happily allow you to ignore exceptions, or catch less or more exceptions than required. Functional error handling corrects this, exceptions do not. Anyway, it's clear we're not going to agree, and you win, since so far the industry is still stubbornly clinging to exceptions, despite them being a failed feature in every language they're in.


It’s not even lazy, your IDE will not do this for you by default.

It’s got nothing to do with getting better. It IS basic Java exception handling. Any proper course or tutorial will tell you to catch specific exceptions


> Any proper course or tutorial will tell you to catch specific exceptions

any real world library or application will catch Exception e, catch checked exceptions and turn them into unchecked ones etc etc


I don't hate checked exceptions because they make me do extra work, I hate them because they inhibit the proper use of inheritance for OO design.


> Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more.

Which is bad. In 99% of cases I have no specific error handling for the given problem. Just let the process crash or be handled by application server / framework.

Representing these kinds of error conditions in the code which I have no interest in handling is just noise.


The problem with that is you lose all ability to understand or control what a given endpoint will return in any given situation

> I have no specific error handling for the given problem

then pass it on and decide what to do with it at the system boundary

in the db:

fun getUser(uuid: UUID) : Either<Error, User>

middle layers: pass around the either, you can map, flatmap etc on it to chain it with other computations there

then in the resource layer

return user .fold({ error -> when(error) { is DbError -> HttpResponse.InternalServerError() is UserNotFoundError -> HttpResponse.NotFound } }, { user -> HttpResponse.ok(user) })

Then, at every layer, each method explicitly says in the method signature what it returns. There’s no need to look around each and every line of every method in every layer, or in the framework, or some global exception handler, to figure out what will happen. Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.

Things like being unable to communicate with the DB or not finding a user are not exceptional, they're entirely expectable when you're calling a db to look for a user and should be modeled accordingly.


> The problem with that is you lose all ability to understand or control what a given endpoint will return in any given situation

Why? In the simplest (but pretty common) case it's:

- successful response

- generic error message

> then pass it on and decide what to do with it at the system boundary

Yes, but if in 99% of cases I only pass it on, it's just visual noise. Noise you become blind to, and it loses meaning.

> Developers can tell from a glance at the resource method what the endpoint will return for each outcome, in context.

They can't really because you end up with some very generic error type anyway. Any typical service can have IOError, DBConnectionError, SQLError, OutOfMemoryError, StackOverflowError plus many other application specific errors. You end up with bulk of your methods returning Either<Error, Something> which is then meaningless.


It feels like you're trying to use Exceptions as a way to steer your logic, otherwise why would you need to know why an operation failed to such detail?

Your controller method cannot act differently on a DBConnectionerror or OutOfMemory error.

Not to mention that exceptions cause developers to use them as control flow mechanisms.

For example searching a user by id. If the database returns 0 records, is that reason to throw an exception? Well, doing result[0] results in IndexOutOfBounds due to result being [].

But the reality is that the user not being there isn't exceptional. Typos are common. By using Result<T, E> or Either you enforce the developers to think more about their flow. One can write the method like this:

   fn find_by_id(id: usize) ->Result<UserResult, Error> -> {
       let raw_db_result = db.search("SELECT id, first_name, last_name FROM user WHERE id = ?", id)?;

       match raw_db_result {
           None => Ok(UserResult::NotFound),
           Some(r) => {
               let only_user = r.ensureOneResult()?;
               let user = mapDBUserToUser(only_user);

               Ok(UserResult::User(user))
           }
       }
   }
What about Error? Reality is that I don't really care. Error doesn't contain anything that is actionable. Either the whole chain succeeds and returns a valid result or the Error. The caller wants to find a user by id. There is one, or there isn't. All the rest is just errors that they'll pass on too. And in the end they get logged and result into Error 500.

A 404 is actually a valid result.

Now, if I were to use a throw new UserNotFoundException() for no user found you end up with generic try catches catching too much. And now someone needs to go and take it all apart to identify that single Exception that they want to deal with separately.

Whereas if I want to add a state in my enum the callers _MUST_ update their code due to how Rust works.


> otherwise why would you need to know why an operation failed to such detail?

I'm not defining the errors like DBConnectionError or OutOfMemoryError - it's the framework/platform which defines them and throws/returns them.

> But the reality is that the user not being there isn't exceptional.

That depends. In some contexts it is not exceptional (getting user by ID given as an argument to webservice), in that case using Maybe type is great. In other contexts it is very much exceptional (e.g. signed JWT refers to a non-existing user) and throwing Exception makes more sense.

> What about Error? Reality is that I don't really care. Error doesn't contain anything that is actionable. Either the whole chain succeeds and returns a valid result or the Error. The caller wants to find a user by id. There is one, or there isn't. All the rest is just errors that they'll pass on too. And in the end they get logged and result into Error 500.

Which is the same as exception. But now you have this visual noise of something you claim you don't care about. An unchecked exception makes this irrelevant noise go away.

> Now, if I were to use a throw new UserNotFoundException() for no user found you end up with generic try catches catching too much. And now someone needs to go and take it all apart to identify that single Exception that they want to deal with separately.

    try {
        ...
    }
    catch (UserNotFoundException e) {
        // handle ...
    }
I'm catching exactly the exception I want. Where I'm catching too much? Where do I need to take it apart?

(This particular example of exception usage is bad, though, as it smells of exception control flow. Here using Maybe type would be better)

> Whereas if I want to add a state in my enum the callers _MUST_ update their code due to how Rust works.

Which is good for some cases where the enum describes "business" cases.

But it is pretty bad for truly exceptional cases (which are unlikely to be handled anyway). Library adding a new exceptional case will break its clients, which seems like a bad trade-off (again, for truly exceptional cases).


```

try {

        ...

    }

    catch (UserNotFoundException e) {

        // handle ...

    }
```

This is just bikeshedding, because the equivalent code in Rust would be. For example, if there's multiple lines in the try block, who exactly returned this error? Are there other errors I didn't handle? Are there unexpected error that I forgot to check. For example, the "get" function of arrays in many languages usually always return T. But this is actually a lie because the array might be empty. So by right, it should return Option<T>. But exception based programming have basically created this illusion that its infallible. How many people check their array accesses?

```

match value {

Ok(ok)=> {...}

Err(UserNotFoundException(e)) = { handle }

e => return e

}

```

Which does look more complicated, but it scales way way better when you have multiple errors

> But it is pretty bad for truly exceptional cases (which are unlikely to be handled anyway).

But why should a language be designed for exceptional cases? Errors are not exceptional at all. In the above code, the actual code will actually look like this

```

let rows = db.get_rows(query)?; // returns Result<Vec<DbRow>, E1>

let first_row = rows.first()?; // returns Option<DbRow>

let user = first_row.to_user()?; // returns Result<User, E2>

return user

```

Exception-based language will have the same looking code, but then imagine what would happen if you try to figure out which functions return what. You have no other recourse other than to dig into the source code to find all the unchecked exceptions that it can throw.

Another example, how would an exception language write this code to get multiple rows from the db and map each row to its respective user.

```

// get_rows and to_user both can fail

let users :Vec<User> = db.get_rows(query)?.map(|r|r.to_user()).collect::<Result<_,_>()?;

```


Actually, I have some java code that handles OutOfMemoryError when I do image resizing. Of the image is so big that the decompressed versión can't fit in half a gig of memory (which happens) then I fall back to some crude subsampling to size it down. It's unnoticeably less pretty but works is how o handle OutOfMemoryErrors in a specific scenario.


Or Kotlin's approach:

   fun getUser(userId: UUID): User?
You cannot treat this result value as a `User` in code that calls this; though, once you null check it in your service layer, you can pass it on as `User` in the not-null branch. Null is nothing to be afraid of if the language forces you to declare nullability and deal with it.


this is a very common in obj-c too

on the other hand, depending on what your trying to do you might want to provide more context about what happened to the user/programmer

in swift you can change a throwing function to a nullable with `try?` so even if `getUser()` throws, you can keep it simple if thats what is appropriate

  guard let user = try? getUser(id: someUUID) else {
      return "user not found"
  }
as an aside, swift "throws" exceptions but these are just sugar for returning basically an Result<T,E> and compose much better than traditional stack unwinding exceptions imo


WirelessGigabit gets it. That last fact is huge - functional error handling forces callers to handle (or pass on) exactly what can go wrong, no more, no less. (unlike exceptions)


That's not really true in practice. Often you just end up with a massive Error variant type that's used everywhere, even when the specific function you're calling could only return one of them.


You don't have to have Error variants if you don't care about their type.

You cannot catch a StackOverflowException or an OutOfMemoryException. All you do is log it & restart the app.

Once could split it into retryable errors and non-retryable ones. Like a DB disconnect, that can be retried.

BUT to be fair, that's not logic that belongs in the business.


You're describing the problems with exception, not using Either... When using Either, you never fold on those errors individually... Anyway, it's clear we're not going to agree, and you win, since so far the industry is still stubbornly clinging to exceptions, despite them being a failed feature in every language they're in.


Situations vary a lot. In general there's no guarantee that a far outer scope is going to know how to deal with errors from deep inside some nested calls except for some very general logic, like failing a whole operation.


Even more commonly, there is no meaningful error handling to be done at a layer above the call.


I don't mean to be uncharitable but it seems like most exception advocates here don't really understand the case us functional error handling advocates are making. We don't advocate handling errors at every level. They're usually sent to the boundary layers of the system, eg the http resource layer, and folded there into http responses.


In Rust you can just do "let value = result.unwrap()".


Especially because Kubernetes and the like work as if the application must die anyway. Pretty much like PHP CGI invocations always worked since 1998 or so.


Exceptions are not unsafe.

That said, not modelling them in the type system is a mistake. But the model has to be useful - knowing what functions can or cannot throw is useful, knowing what they throw, less so.

(They are safe because of try-finally / try-with-resources)


They are unsafe because they invariably result in people either ignoring them or catching more than they should, or less than they should, and the compiler happily lets you do that, EVEN when you're using checked exceptions.


How will the compiler let you catch less than you should when using checked exceptions?


There is no should/shouldn't. If you don't have a specific error in mind and how to handle it, you shouldn't handle it. In practice most errors can be left unhandled all the way to e.g. server response, so this works quite fine. `try-with-resources` is typically awkwardly implemented unfortunately (defer is nicer, the new `using` keyword in JS is quite nice too)


I don't mean to be uncharitable but it seems like most exception advocates here don't really understand the case us functional error handling advocates are making. We don't advocate handling errors at every level. They're usually sent to the boundary layers of the system, eg the http resource layer, and folded there into http responses. The safety case we're making has nothing to do with try catch finally, it is about signatures and what callers are forced and allowed to do.


> Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more.

That's what wrong with them. Callers, in almost all cases, do not know what to do exceptional outcomes. So you force programmers into a situation where they're dealing with things they shouldn't be dealing with.


You mean something like C++ Optional, right? Those are nice, if the language supports generics.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: