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.
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).
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.
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.
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.
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:
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.