4 ms·
Does this argument hold: This pushes the error handling away from the call site, and that is a bad thing. Being that the caller knows best how to handle the err
by bongobingo1 3y ago
Does this argument hold: This pushes the error handling away from the call site, and that is a bad thing. Being that the caller knows best how to handle the errors, so it should instead of passing them down.
(Full disclosure, I have seen this talk and read Wlaschins book, but not in a long time, so maybe he did cover this and I forgot.)
Eg given:
validate
and-then update-db
and-then send-email
What about when validate fails? The error is returned to the caller, which is
maybe suitable. What about when update-db fails? Should there be a retry? Try
another service? Requeue? Tell the user? What about if the send-email fails?
Dont you end up with (except with more unwrapping and sub branches)
validate
and-then update-db
and-then send-email
and-if (Ok, ok)
or-if (ValidationError, notify-user)
or-if (DBError, retry-in-5 or-then retry-remote or-then notify-user)
or-if (EmailFailed, requeue-email and ok)
Maybe thats ok?
- Akronymus 3y agoYou can wrap the error types into a discriminated union and then check for retry-able errors, and retry if its one of those, otherwise, propagate the error. Or if you don't need to do special handling for any errors, you can just propagate the whole error and let the caller handle it. ROP definitely isn't useful in all cases, but it has served me quite well in many by simplifying error handling when I don't care at the callee what error occured.
- cjfd 3y agoI think it should be pretty obvious that for instance retry logic should be near the place in the code where the first attempt is made. The best case for gathering all errors in a 'railway' is when they can be handled uniformly. E.g., if we want to not retry things but instead report a descriptive error message to the user. Note that also that most error handling logic that should be near the calling place might also fail in more global ways that are then best handled using the railway/exception pattern.
- piaste 3y agoLow-level and high-level error handling can coexist. It's no different than having a specialized try/catch inside a larger try/catch. You may, for example, choose to handle very fast transient errors inside your `send-email` function. If you manage to connect in under $acceptable-time, return to the happy path*, otherwise return to the caller. The only hard and fast rule is that any handling requiring human intervention should definitely be wrapped up and passed up. * but do log a warning, regardless of whether you're just doing classic impure logging, collecting the logs as part of the success value, or using a writer monad.
- ilyt 3y agovisually the classical try-catch makes error handling right there more verbose and less readable. I far prefer Rust-like passing result with maybe adding context to the error along the way than try-catch block at every step of long process that have errors that should be handled in-place. Because passing it up is still easy, but adding any context is much less verbose. > The only hard and fast rule is that any handling requiring human intervention should definitely be wrapped up and passed up. The "wrapped up" being the important and mostly ignored part. From my experience try-catch error handling usually generates incomprehensible errors where you can only guesstimate what actually happened from stack trace's function names. To get good error message every layer should have try-catch block that adds context to the error (what operation did it, or what logged user did it etc.), but that's almost never done properly and we get stack trace vomits instead. If you want it to be fixed by actual humans you need to
- mrkeen 3y agoMonadic error-handing a win-win way of doing things. People who dislike Go complain about being forced to check for error conditions too often (likewise with Java's checked exceptions.) Using 'Either', you're not forced into checking; you can check, or you can let the caller handle it. People who dislike Java's unchecked Exceptions complain that there's no way of knowing what will be thrown, or when. Using 'Either', this is made explicit. > What about when validate fails? The error is returned to the caller, which is maybe suitable. What about when update-db fails? Should there be a retry? Try another service? Requeue? Tell the user? What about if the send-email fails? The point is - you don't know! So your approach needs to be well-suited to not knowing, which Either is. In my current Java codebase at work, there are different ways of 'handling' errors which have built up over the years. A call to a missing 'Limit getUserLimit(User)' might: * throw a NotFound exception * return null * return a default And you can't tell without diving in and reading the code. If it had been written with Either instead: * the caller could trust it instead, rather than reading all the code that it calls * easily convert it to another failure condition - the Either implementation in your language will have built-ins for 'orElse(null)', or 'orElseThrow(...)' > Should there be a retry? Either is an expression, which lends itself well to abstraction. This means you can likely write code to retry Eithers in general, as opposed to writing retry code for specifically inside your DbUpdater.
- DanielBMarkham 3y agoExcellent comment. Read my mind. Yes, low level and high level error handling can coexist, but this style of programming works much better (in my opinion) as a blackboard exercise or a do-it-once-then-done situation. Love me some DDD, but you gotta have some kind of reasonable maintenance cycle that a moron programmer like myself can manage. That was the beauty of TDD in mutable coding: it scoped down the cognitive load for maintenance. I can see railway programming carrying a lot of context through a lot of transforms and hell if I'd want to be at the end of the railway getting a big trainload of business and system context I'm unprepared to handle. DDD and onion architecture for the win. Best of both worlds, and you end up with a railway anyway; it's just outside the micro-apps, not stuck inside them. In my mind, you want railway issues both explicit and a bit cumbersome to code. Most of the time, if done well most railway coding decisions involve business decisions that you should never be using clever coding constructs to avoid in the first place. ADD: To clarify, monadic programming is great but there's a temptation to use it to avoid necessary business decisions. Sticking ambiguity into a type system can lead you to some difficult or impossible situations later on. I'm not saying never do it. I'm saying most ways I've seen it done are not so good.
- sasmithjr 3y agoIt's possible that you could end up with your example as given, but I think you could organize it differently so that it has your handling while also maintaining the same basic top-level pipeline. Given what you've specified, I might organize it like: notify-user: if (ValidationError, notify-user-validation-error) or-if (DBError, notify-user-db-error) try-update-db: update-db or-then retry-in-5 or-then retry-remote try-send-email: send-email or-then (requeue-email and ok) do-root-level-pipeline: validate and-then try-update-db and-then try-send-email or-then notify-user I think I correctly mapped the and/or/thens to map/bind, so I hope that makes sense.
- AndrewPGameDev 3y agoI tried implementing this logic in java with a try-catch style: try { Validation validation = validate(); try { Value value = update_db(validation); send_email(value); } catch (DBError db) { try { sleep(5); Value value = update_db(validation); send_email(value); } catch(InterruptedException ie) { // do nothing, stupid java-ism } catch(DBError db2) { try { Value value = update_db_remote(validation); send_email(value); } catch (DBError db3) { notify_user(db3); } catch (EmailFailedError e) { requeue_email(e); } } catch (EmailFailedError e) { requeue_email(e); } } catch (EmailFailedError e) { requeue_email(e); } } catch(ValidationError v) { notify_user(v); } https://gist.github.com/Andrewp2/9d97bd213b061166d6df565ce26dc838 https://gist.github.com/Andrewp2/9d97bd213b061166d6df565ce26... IMHO this is much worse than the railway version you gave, but maybe someone could write a superior version that still uses try-catch.