Well we are in a discussion thread about a language that does just that :)
I see two issues with the `?` operator:
1. Most Go code doesn't actually do
return nil, err
but rather
return nil, fmt.Errorf("opening file %s as user %s: %w", file, user, err)
that is, the error gets annotated with useful context.
What takes less effort to type, `?` or the annotated line above?
This could probably be solved by enforcing that a `?` be followed by an annotation:
val := doAThing()?("opening file %s as user %s: %w", file, user, err)
...but I'm not sure we're gaining much at that point.
2. A question mark is a single character and therefore can be easy to miss whereas a three line if statement can't.
Moreover, because in practice Go code has enforced formatting, you can reliably find every return path from a function by visually scanning the beginning of each line for the return statement. A `?` may very well be hiding 70 columns to the right.
For the first point, there are two common patterns in rust:
1. Most often found in library code, the error types have the metadata embedded in them so they can nicely be bubbled up the stack. That's where you'll find `do_a_thing().map_err(|e| Error::FileOpenError { file, user, e })?`, or perhaps a whole `match` block.
2. In application code, where matching the actual error is not paramount, but getting good messages to an user is; solutions like anyhow are widely used, and allow to trivially add context to a result: `do_a_thing().context("opening file")?`. Or for formatted contexts (sadly too verbose for my taste): `do_a_thing().with_context(|| format!("opening file {file} as user {user}"))?`. This will automatically carry the whole context stack and print it when the error is stringified.
Overall, what I like about this approach is the common case is terse and short and does not hinder readability, and easily gives the option for more details.
As for the second point, what I like about _not_ easily seeing all return paths (which are a /\? away in vim anyways), is that special handling stands out way more when reading the file. When all of the sudden you have a match block on a result, you know it's important.
It might just be me, but I find both of those to be massively less readable. More terse is not the same as more readable (in fact, I find the reverse).
I'm a huge fan of keeping things simple; my experience has shown me that complex things have lots of obscure failure points, while simple things are generally more robust.
You always have the option of using a match block if you don't like those chained calls. But I do agree, it's a bit bolted on and kinda ugly.
> More terse is not the same as more readable (in fact, I find the reverse).
I generally agree, but I also find that "all explicit" also hinders readability because it tends to drown the nitty-gritty details. As always it's a matter of balance :) And I think that neither go nor rust are great in this matter as one is verbose and the other falls in the "keyword soup" with the chain call, the closure, and the format macro. I'm pretty sure something in between could be found.
I see two issues with the `?` operator:
1. Most Go code doesn't actually do
but rather that is, the error gets annotated with useful context.What takes less effort to type, `?` or the annotated line above?
This could probably be solved by enforcing that a `?` be followed by an annotation:
...but I'm not sure we're gaining much at that point.2. A question mark is a single character and therefore can be easy to miss whereas a three line if statement can't.
Moreover, because in practice Go code has enforced formatting, you can reliably find every return path from a function by visually scanning the beginning of each line for the return statement. A `?` may very well be hiding 70 columns to the right.