3 ms·
> 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 pre
by 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.
- WirelessGigabit 3y agoYou 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.
- The_Colonel 3y ago> 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).