On Mon, Oct 24, 2022 at 7:18 PM Ian Lance Taylor <[email protected]> wrote:

> On Sun, Oct 23, 2022 at 9:31 PM 'Daniel Lepage' via golang-nuts
> <[email protected]> wrote:
>
> ...
>
>
> > 3. Streamlining shouldn't only apply to error handling that terminates
> the function.
> >
> > Unlike panics, errors are values, and should be treated as such, which
> means that the calling function should be able to decide what to do, and
> this should include continuing. Frequently it won't - a lot of error
> handling is just return fmt.Errorf("more context %w", err) - but any
> proposal that assumes that it *always* won't is, IMO, confusing errors with
> panics. This is the question that first started this thread - I didn't
> understand why all the existing error proposals explicitly required that a
> function terminate when it encounters an error, and AFAICT the answer is
> "because people are used to thinking of errors more like panics than like
> return values".
>
> For what it's worth, I see this differently.  The existing language is
> not going to go away, and it's pretty good at handling the cases where
> an error occurs and the function does not return.  Those cases are by
> their nature all distinct.  They are not boilerplate.  The way we
> write them today is fine: easy to read and not too hard to write.
> When people writing Go complain about error handling, what they are
> complaining about is the repetitive boilerplate, particularly "if err
> != nil { return nil, err }".  If we make any substantive changes to
> the language or standard library for better error handling, that is
> what we should address.  If we can address other cases, fine, but as
> they already work OK they should not be the focus of any substantive
> change.


Ok, that makes sense, and I think answers my initial question - all recent
error-handling proposals have been termination-based not because anyone is
specifically *against* streamlining continuation cases, just because it's
also not important to anyone.

> Is part of the problem that the discussion around the
> try/check/handle/etc. proposals got so involved that nobody wants to even
> consider anything that looks too similar to those? Would it be more
> palatable if I proposed it with names that made it clearer that this is
> about the consolidation of error handling rather than an attempt to replace
> it entirely?
> >
> > onErrors {
> >     if must Foo(must Bar(), must Baz()) > 1 {
> >       ...
> >    }
> > } on err {
> >    ...
> > }
>
> While error handling is important, the non-error code path is more
> important.  Somebody reading a function should be able to easily and
> naturally focus on the non-error code.  That works moderately well
> today, as the style is "if err != nil { ... }" where the "..." is
> indented out of the normal flow.  It's fairly easy for the reader to
> skip over the error handling code in order to focus on the non-error
> code.  A syntactic construct such as you've written above buries the
> lede: what you see first is the error path, but in many cases you
> actually want to focus on the non-error path.
>

Sorry, "onErrors" was a poor choice of word to replace 'try'; I wasn't
proposing putting the handling before the non-error code, just wondering if
using different names for try/check/handle would help. Using my original
proposal but with must/on instead of check/handle, code would look like
this:

try {
    if foo, limit := must Foo(), must Limit(); foo > limit {
        if foo = must DiminishFoo(foo); foo > limit {
            return fmt.Errorf("foo value %d exceeds limit %d, even after
diminishment", foo, limit)
      }
  }
} on err {
  return fmt.Errorf("enforcing foo limit: %w", err)
}

You can read the normal flow pretty easily here - you compute a value and a
limit, and if the value exceeds the limit you try to reduce it, and if it
still exceeds the limit you return an error. Foo(), Limit(), and
DiminishFoo() all return (value, error) pairs, but we don't consider this
part of the "normal" control flow, so the code that handles if any of them
fail is separated into its own block slightly further down.

In contrast, the modern Go version buries the "normal" flow in a pile of
error handling, and IMO is a lot harder to follow:

foo, err := Foo()
if err != nil {
    return fmt.Errorf("enforcing foo limit: %w", err)
}
limit, err := Limit()
if err != nil {
    return fmt.Errorf("enforcing foo limit: %w", err)
}
if foo > limit {
    foo, err = DiminishFoo(foo)
    if err != nil {
        return fmt.Errorf("enforcing foo limit: %v", err)
    }
    if foo > limit {
        return fmt.Errorf("foo value %d exceeds limit %d, even after
diminishment", foo, limit)
    }
}

This is fundamentally what I'm trying to propose - I'm not trying to
address the existence of boilerplate, just trying to make it easier to move
identical error handling blocks to the end so that the normal flow is more
visible.

I have no idea if "must" is better than "check" - it's maybe clearer that
it's going to jump to error handling if it fails, but it also might be
confusing given that existing funcs like MustCompile use panics instead of
errors.

Thanks,
Dan

-- 
You received this message because you are subscribed to the Google Groups 
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/golang-nuts/CAAViQtireR4kd-P8KRQAubD9Nx4rLyzd3c0phEgbm3d50MRB6w%40mail.gmail.com.

Reply via email to