4 ms·
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 hand
by bedobi 3y ago
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_Colonel 3y ago> 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.
- bedobi 3y agoYou'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.
- WirelessGigabit 3y agoIt 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.
- bedobi 3y agoWirelessGigabit 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)
- wtetzner 3y agoThat'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.