6 ms·
Swift, and I imagine other modern languages, do this the right way. Their idiomatic error passing is via try/catch, which is the best error handling pattern we'
by happytoexplain 2y ago
Swift, and I imagine other modern languages, do this the right way. Their idiomatic error passing is via try/catch, which is the best error handling pattern we've figured out so far, but instead of bloating the API for every case where somebody might theoretically not care about the specific error and isn't doing multiple failable operations in close proximity, it just adds some sugar to get the result described in this article:
`let fooOrNil = try? Foo("blah")`
Edit: Updated URL to Foo, since Swift happens to use optional return for its own 'URL' type, as pointed out.
- nox101 2y agoThat's arguably a poor use of exceptions. Swift has URL.init which returns nil if parsing fails. It doesn't throw.
- happytoexplain 2y agoI only meant to illustrate the feature, not argue that this specific constructor should use exceptions vs optional return. Regardless: Why would it be a bad use of exceptions? It reduces the API size and unifies flows, and gives me the option of seeing the specific error. It simply happens to be that in practice I've never needed to know the specific error in this case.
- taspeotis 2y agoIn .NET it’s idiomatic to avoid exceptions for control flow because “it failed to parse” means creating an exception which costs you an allocation and getting the stack trace, among other things.
- throwaway2037 2y ago> means creating an exception which costs you an allocation Please. What percent of .NET users are really worried about that allocation? Maybe 0.01%?
- neonsunset 2y agoExceptions are meant to represent exceptional states. The data is always expected to be valid and there is no recovery? .Parse The data may be invalid and the implementation can handle it? .TryParse This is a very common pattern and adopted for reasons besides the cost of exceptions. Though in this case it is Uri.TryCreate instead.
- chuckadams 2y agoThe allocation is plenty cheap, but creating the stack trace is expensive in pretty much all structured exception systems, and it is noticeable in loops, not necessarily even really tight ones. So yeah if you’re parsing thousands of url candidates and expect many will not parse, don’t use exceptions to communicate the expected errors. Otherwise don’t worry about it.
- positr0n 2y agoExceptions are nice DX, but theoretically if you were parsing URLs in an inner loop and expecting lots of them to fail it would be very expensive compared to returning a tuple (or Optional, whatever).
- throwaway2037 2y agoIf it doesn't return an error code, nor throw an exception, how do you know why it failed? Man, the resistance to exceptions with their human-readable error messages and stack traces and sorely misunderstood on HN. Any time someone says "exception", 50 people show up to point at the boogey man. Why all the fear? I grew up programming in the era of "errno only" (or god forbid: Win32's GetLastError()). It was awful. Exceptions are so much more human-friendly. Most programmers are writing CRUD apps for a business. Exceptions are terrific for this case.
- mrkeen 2y ago> Man, the resistance to exceptions with their human-readable error messages and stack traces and sorely misunderstood on HN. You grew up with "errno only", so that's your frame of reference. You've seen exceptions and don't want to go back to errno. Do you "fear" errno? Is errno the "boogey man"? I've seen monadic error handling and I don't want to go back to exceptions.
- tredre3 2y agoThere's a range of other options for error handling between a global errno and wrapping every lines in try-catch... Many languages make it idiomatic for functions to return a value or error (Rust, Go, modern C++) so that you can access a human-friendly error on failure. https://doc.rust-lang.org/std/result/ https://doc.rust-lang.org/std/result/ https://go.dev/doc/tutorial/handle-errors https://go.dev/doc/tutorial/handle-errors https://en.cppreference.com/w/cpp/utility/expected https://en.cppreference.com/w/cpp/utility/expected (there's also std::variant and std::optional that can get you there since C++17)
- kaba0 2y agoGo pretty much uses a global errno, it’s literally analogous, and is a terrible language from an error handling perspective, that failed to learn anything since C.
- presentation 2y agoI much prefer Maybe types to try/catch.
- happytoexplain 2y agoThey're not really comparable. They serve different purposes. Maybe/Optional is for when something might not be, while try/catch is a powerful system for passing both user-facing and machine-readable information about errors across potentially multiple layers of API. A powerful language has both. Saying "I prefer Maybe to try/catch" is too broad - it's almost like saying you prefer not to handle errors.
- presentation 2y agoWell, you handle errors by needing to explicitly handle them at the point of error, as opposed to all callers needing to anticipate and understand every random system error from 10 levels deep of APIs (or just throw them all away).
- XorNot 2y agoExcept this isn't my experience of errors at all. My experience of errors is that most of the time you need more scope in order to handle them properly. I.e. if writing to a file happens, what am I actually supposed to do about that? Well, that depends entirely on why I'm writing to a file. Which might not be information the calling function actually has. Usually I need to go up the call stack to find somewhere which has enough context to know what's actually not working because a file write has failed. Basically, the vast majority of my code can always error or depends on things which can: or is one refactor away from now needing to return errors. If just about everything is Result<T>, then really why should anything be? Why eat the syntactic noise?
- Mawr 2y agoOf course you think the majority of your code can error - exceptions don't let you draw a distinction between fallible and infallible code, so you have to assume all code can fail. This leads to two problems: 1. You can't take advantage of infallible code... Infallible code is much easier to use because there is no need to worry about errors. But you can't take advantage of that because infallibility is not encoded in any way. With Result types, infallible code is clearly marked, which allows for sectioning it off from fallible code. Programmers naturally gravitate towards doing just that because they want to take advantage of the ease of use. The result is a lot of pure functions and as little fallible code as possible. 2. ...but you will erroneously try to do so anyway You will inevitably start assuming that certain code can't possibly fail. The ease of use of infallible code is just too alluring: [1] [2] [3]. [1]: https://nedbatchelder.com//blog/202001/bug_915_solved.html https://nedbatchelder.com//blog/202001/bug_915_solved.html [2]: https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36693 https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36... [3]: (a little wordy) https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/ https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
- golergka 2y ago> which is the best error handling pattern we've figured out so far What are the reasons you think it's better than Either and map, especially for expected control flow that doesn't signify programmer error and just responds to bad input data? I can't see any upsides. The function doesn't force the caller to handle the error, and the error isn't type checked: if the function stops throwing one type of exception or starts throwing another, type system won't force outdated call sites to be fixed.
- maxbond 2y ago> Their idiomatic error passing is via try/catch, which is the best error handling pattern we've figured out so far, ... I find monadic error handling (`Result<T, E>` and `Option<T>`, or `Either<T, U>` and `Maybe<T>` if you prefer Haskell) to be much better at keeping error handling logic local to where errors originate, and making it explicit which operations are fallible and what errors they produce. Exceptions tend to be nonlocal and difficult to determine what exceptions can occur from where. That sugar for getting an optional/nullable value with `try?` definitely looks nice, I wish Python had something like that, but it seems to me that the reason it works better is that it's closer to monadic error handling (you get an `Option<T>`). Has your experience been different?
- happytoexplain 2y ago>making it explicit which operations are fallible and what errors they produce I should specify that throwability needs to be part of the function signature to be useful in the language. As for what types of errors: At minimum they should provide something you can show a user, maybe after adding context, and the machine-readable codes should be documented, both of which are points of failure, but the ability is there. Interestingly, Swift is getting typed exceptions, which seems too good to be true. I'm not sure how it works when passing exceptions up the call chain. As for Optional/Maybe, as I replied to a sibling, that's not really comparable. It's not for error-handling because it doesn't provide an error. It's useful for when something might not exist, but not so much for when some input is invalid, etc, since it's more verbose to explicitly fork on null and return early. As for Result<T,E>, it has a subset of the same problems: Can't pass it up without writing a fork/switch unless it's just a wrapper function that returns Result<T,E>. And it makes things less composable or require extra forks when something is expecting T. But, like Optional, it definitely has uses: Like if you're using a callback system for an async function, where throwing is impossible.
- maxbond 2y agoI agree that exceptions are much more tolerable when they're part of the method signature. Otherwise you have to dig through the code to figure them out, and that's a mess. You can think of `Optional<T>` as being `Result<T, null>` - it's a fallible operation where there is either one obvious way it can fail or where we don't care to distinguish between them. If you look a key up in a map, that's a fallible operation, but there's only one way for it to fail - the key wasn't present in the dictionary. It's definitely error handling. `Result` really comes into it's own when combined with pattern matching (`switch` with destructuring and exhaustiveness, so compilation will fail if you've neglected to handle a certain error case (!!!)). Rust adds syntactic sugar and other features that make this very ergonomic. Eg, there's a `?` operator that will return early if there is an error, and traits (interfaces) that make it seamless to convert one error into another, add error messages, etc. > it makes things less composable Not so! They're very composable, eg: let x = my_fallible_operation().map(|v| do_something_on_success(v)).or(other_fallible_operation_to_compose_with()) If `my_fallible_operation()` success, `x` will be `Ok(do_something_on_success(v))`; otherwise, it will be the return value of `other_fallible_operation_to_compose_with()` (itself a result).
- randomdata 2y ago> error passing is via try/catch, which is the best error handling pattern we've figured out so far The best error handling pattern for scripts we've figured out so far, but at the same time the worst error handling pattern for systems known to man.
- brabel 2y agoI disagree. I write very large code bases in Kotlin and Java. Kotlin did away with checked Exceptions and it's a pleasure to use, without any significant decrease in the quality of the systems compared to Java, quite the opposite because Kotlin forces you to check stuff like nullability. I've also used Rust and I simply can't see any real improvement in the reliability of code I've written just because it forces you to use Result type (it's just too easy, and tempting , to swallow an Error or panic with `unwrap` because otherwise you just end up with every single function returning Result, making it really non-egornomical to work with... arguably the Rust language itself did that with arithmetic operators).
- randomdata 2y agoIt seems you agree. Systems and scripts are not differentiated by the size of the codebase. The way you speak about errors, like "being tempted to swallow errors" and "returning Result from every single function", suggests that you are accustomed to writing scripts. Which isn't unexpected as a lot of computing workloads are script in nature. And, indeed, the 'try/catch' model is well suited to it. But it not fun when writing systems. That is why we have different tools for different jobs.
- brabel 2y agoYour definition of "script" is completely different from what most people would use and honestly makes no sense.
- randomdata 2y agoThat's fine. Obviously words can mean anything and naturally one would ensure that they are aware of what they are intended to mean in a given context before going any further if there is any lack of clarity. As there was no earlier questioning of what they might mean, there clearly was no issue with clarity. To backtrack now is strange and hilarious. But I posit that the "definition" is consistent with the literature. Scripts carry out a defined set of tasks and if something goes wrong they can simply fail, wait for the problem to be resolved, and then try again. Systems don't have the same luxury. They have to find alternative solutions when fault occurs. "An error occurred. Try again later." is not acceptable for systems, but is acceptable and perfectly valid for a large class of computing problems. Again, "try/catch" is great for scripts as most of the time you just want the error to "bubble up to the top" to notify the user, or whatever, anyway. Such a thing would be unthinkable in systems, though. When you actually have to deal with errors at every turn, "try/catch" is a slog, and I would argue a poor mental model to boot (what's special about errors that deserves something different?). It turns out that software development is an engineering endeavour, not a religious one. Different tools for different jobs.
- happytoexplain 2y agoBlanket update: I have been referring to try/catch error-handling, but have used the word "exception" as a synonym a couple times, and I think a couple other people have made the same assumption of synonymity in their replies. Really, "exception" should be reserved to refer to things that "shouldn't happen", that we don't want to waste resources recovering from. In most languages, try/catch is exclusively for exceptions, and they are sometimes/always unchecked, but in Swift, try/catch is for plain errors, and the compiler forces you to handle them. This is a huge caveat, and a mistake on my part, that makes 50% of the conversation moot. I am only lauding try/catch error-handling, and checked i.e. enforced try/catch at that, but not exceptions.
- slaymaker1907 2y agoI think I agree on that, but returning null seems like a bad idea in this specific instance. Instead, I think I'd prefer they return an object like a promise so people are forced to check if an error occurred.