`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.
`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.
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?
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.
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).Are you saying that Result<T, E1> doesn't convert to Result<T, E2> automatically? Cause I've had great success implementing `From<E1> for E2` and forwarding the error via `?`. There's also map and unwrap helpers to convert. If you're saying that Result<T, E> is incompatible with a return type of T, that's often seen as a benefit because they are incompatible and you have to choose how to bridge that gap by for example unwrapping because you don't expect an error (equivalent to not specifying a catch and letting the thread terminate) or somehow else doing something with that error (specifying a catch handler). What it doesn't let you do is ignore the failure case silently and implicitly.
This is true statically (mitigated by checked exceptions in Java, but that's a whole can of worms on its own), but the opposite is true when you actually have an error at runtime—exceptions come bundled with stack traces which make it very easy to see the stack of function calls that get us into the failure state, where with result types you at best have a (hopefully descriptive) error message and that's it.
If the code has already been well polished then that error message is probably enough, but if you're still ironing out rough edges the error message itself may be in error and the actual problem may be somewhere else entirely. With a stack trace you can easily see the function calls that got into that unexpected state, but without them you're on your own and have to try to recreate the error while stepping through in the debugger.
(My perspective is Rust centric, and if you eg unwrap an error because you mistakenly thought it shouldn't be reachable, you end up with a panic which is just an exception.)
This has not been my experience with Rust. In my experience I'll handle all the errors just fine—nothing is unexpectedly crashing—but then every once in a while an error object will propagate up and I'll have no clue why that error got triggered by that input. If I'm lucky I'm working on something that makes dropping into the debugger easy, but other times this is the beginning of a very long troubleshooting session that would have been much shorter if I had a stack trace.
Mind you, this always does turn out to be a mistake in my code that caused an error to get returned incorrectly, but Rust doesn't help me find and fix these problems the way that Java does.
(My personal opinion is that Java's checked exceptions were on the right track and just need some UX improvements and syntactic sugar for handling them. They provide all the safety of a result type with all the debugability of an exception.)
ETA: Rereading I see you meant that an error was returned from the wrong code path. That is interesting, and I do see why you're lamenting the lack of a stack trace in that situation. Maybe there's some way to write a library like `thiserror` that includes a lightweight, application level "breadcrumb trail" that's cheaper than a stack trace but with some of the observability improvement, I'm not sure. Maybe I'm just reinventing logging.
Except that it does not matter whether exception is checked when reflection is used. All you get is unchecked InvocationTargetException that wraps the actual exception.
To the best of my knowledge method signatures in JVM do not contain declarations of thrown exceptions, that's Java thing. Java being Java, all the sparkly modularization and stuff means that effectively calls to entrypoint methods are hidden behind `invoke()` anyway. Or compiled against binary libraries.
Checked exceptions only work as supposed to only under very specific circumstances, usually don't and that is by design - Java/JVM features do not interact with each other nicely.
I don’t see your point. Reflection is not the primary way of accessing methods, it’s used similarishly to unsafe in Rust, you have a safe wrapper around them. Its exception type pretty much means that you knowingly entered here, proceed with care.
As for JVM-interop, sure, checked exceptions themselves are only a java compile-time feature. I don’t really see it coming up all that much though, you can easily wrap in some language-specific way, but java can be much more lenient about error cases like this — you can at worst just do a catch-all at the top layer, and restart the operation. The VM can’t go off the happy path in any case. This is unlike rust’s unsafe, for example, which only needs a single unintended data race, and afterwards all bets are off.
func someThrowingFunc() throws -> T
let someResult = Result(catching: { try someThrowingFunc() }) // someResult: Result<T, any Error>
Or the other way: let someResultFunc() -> Result<T, any Error>
let t = try someResultFunc().get() // t: T
The reason I say they’re not “real” exceptions is that it compiles down to a branch that checks if a particular error return register is non-nil, same as you would with a result type. (As opposed to “real” exceptions like in C++ where execution is forcefully interrupted by an expensive exception handler.)I started out in swift treating it like rust and using Result everywhere, and manually chaining results back up when I didn’t want to handle them, but after a while I realized that the `throws` and `try` statements were just way more readable syntax to accomplish the same thing I was already doing:
Want to “not care” about the thrown error? Use `try?`. Want to propagate it up? Use `try` and just mark your function as throws rather than explicitly returning a Result. Want to crash on an error? Use `try!`. Want to explicitly deal with the error? Use `catch`, or if you don’t like that syntax and you’d prefer matching on a success/error enum you can just convert it to a Result as I outlined above.
As for sum types, it is definitely not good practice, but I would say that if you fail to handle an error condition close to its source, and it managed to get to some “global” scope, determining where it originates is very hard. Similar to that illegal database row that somehow managed to get entered, resulting in a difficult investigation.
I think it's fundamentally less local to do a conditional jump elsewhere in the code (even if doesn't cross into a different frame) than to return a value and step to the next line, rain or shine. I don't disagree that there's an isomorphism between the approaches (since they're both addressing the same need, surely there should be), but exceptions introduce a new set of rules whereas monadic error handling repurposes the familiar semantics of function calls.
But you're not the first person to mention your second point, so I totally concede that, regardless of whether it is "bad practice," empirically it's a weakness of the approach.
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.
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.
> means creating an exception which costs you an allocation
Please. What percent of .NET users are really worried about that allocation? Maybe 0.01%?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.
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.
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.
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://go.dev/doc/tutorial/handle-errors
https://en.cppreference.com/w/cpp/utility/expected (there's also std::variant and std::optional that can get you there since C++17)
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.
Saying "I prefer Maybe to try/catch" is too broad - it's almost like saying you prefer not to handle errors.
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?
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
[2]: https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...
[3]: (a little wordy) https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
The only code I care about is code which can error, or worse, code which is expanding due to a requirements change and is about to make the transition to "needs to be able to return errors".
There are vanishingly few business logic processes that can't error, because every bit of disk IO, network IO and user input can always throw an error that needs to be handled.
What I am saying is that the benefits of pure functions are obvious, but the number of practical applications of them is far smaller then their advocates claim. They don't save me from the fact that the network may be down, the target device might crash, someone might knock the power out at the other end, or the network card picks that moment to corrupt a packet.
What is the point of spending a lot of syntax on Result<T> types returning all over the place, rather then just throwing a try-except around the places in my code I know I can reasonably bound and define what the recovery process is if an error does occur. Which for every non-toy example, is a guarantee.
Rust kind of cheated a bit with arithmetic and it doesn't return a Result when you use division, for example (it's cheating because if not for convenience, it really should return Result as arithmetic errors like division by zero and reaching a NaN are fairly common - and hence would justify that).
If you were forced to handle exceptions at every call site I might find myself able to agree, but then you have something functionally equivalent to options. I'd much rather just start with that approach.
If you write library code, fail polymorphically, then the client can choose whether they want to call it as Maybe/Either/Exception and we can stop these never-ending arguments.
Maybe/Either/Result is a way to represent partial computations at runtime.
try/catch is a syntax to detect partial computations and to do something conditionally if one occurs.
You could have a language that used try/catch syntax to catch Result types, indeed Rust does something like this with the try operator although it’s only try/catch if you squint very hard.
fn returnsAResult() -> Result<i8, Error> {
return rand(0,1) ? Ok(4) : Err(BadLuckError)
}
do {
let x = try returnsAResult()
} catch e {
print(e)
}
This language doesn’t exist, but it could.Results are in-band errors where the error is a normal return value. Swift by contrast uses out-of-band errors.
Try/catch is one particular syntax for detecting errors. Match syntax is another. In principle there’s nothing preventing match syntax being used with out-of-band errors, even with Java-like exceptions. It would look something like this:
match (iCanThrow()) {
case 42 -> …
catch e -> …
_ -> …
}
I like this one less than the first one though.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.
But it not fun when writing systems. That is why we have different tools for different jobs.
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.
"... a script is a relatively short and simple set of instructions that typically automate an otherwise manual process."
You keep talking about "system" as if there was also a clear definition for that in software, which makes me think you're not even a software engineer at all, are you?
> Obviously words can mean anything
No, that's your mistake. Words can't mean what you choose they to mean! Do you know the old meme "Stop trying to make fetch happen!"?? So, "system" wont' happen!
I guess that you're talking about embedded development??? Where Exceptions are not used because they're a bit too costly (and because they're written in C which has an incredibly primitive error handling, it doesn't use Result types anywhere), NOT because they make software unreliable. You may disagree with that, but for goodness sake stop with your condescending tone pretending that you know something you clearly don't and work on "systems" which we, mere mortals (despite you having zero idea what kind of "systems" I work on), have no idea about.
Most tasks humans perform are in following a "script" so stands to reason that software would carry that forward. Why wouldn't most code be of the script variety? It would be quite surprising if that wasn't the case.
> which makes me think you're not even a software engineer at all, are you?
Appeal to authority and ad hominem at the same time. Intriguing.
> I guess that you're talking about embedded development???
I am talking about scripts and systems. You had this figured out earlier when you linked to Wikipedia. It was right there in front of you. Why the confusion again all of a sudden?
It is probably true that embedded device work is generally system in nature. It is likely that their typical workload isn't amenable to scripts, although there are some obvious exceptions, so perhaps that is what you had in mind there? But systems certainly aren't limited to embedded work.
> Where Exceptions are not used because they're a bit too costly
While I respect your ability define words as you see fit, if you really want to be the word pedant, we're talking about errors, not exceptions. Specifically carrying errors via exception handlers. It is not unusual for embedded systems to store exceptions, even in C, but that is well beyond the topic at hand.
It is always funny when the pedants aren't even technically correct. Well, at least you tried. You will always have that.