Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: The #id/catch error model, a rethink of check/handle #27519

Open
networkimprov opened this issue Sep 5, 2018 · 39 comments
Open

proposal: Go 2: The #id/catch error model, a rethink of check/handle #27519

networkimprov opened this issue Sep 5, 2018 · 39 comments

Comments

@networkimprov
Copy link

@networkimprov networkimprov commented Sep 5, 2018

Please do not down-vote this post if you are against any new syntax for error handling.
Instead, vote in the first comment below. Thanks!

Having heard users' frustrations with Go1 error handling, the Go team has committed to delivering a new method. Ideally, a solution would stem from a familiar language. The Go2 Draft Design is fine for wrapping errors with context, and returning them succinctly. But its feel is novel, and it has significant drawbacks, discussed in Golang, how dare you handle my checks!

Besides returning errors, Go programs commonly:
a) handle an error and continue the function that received it, and
b) have two or more kinds of recurring error handling in a single function, such as:

{ log.Println(err) }
{ debug.PrintStack(); log.Fatal(err) }
{ if err == io.EOF { break } }
{ conn.Write([]byte("oops: " + err.Error())) } // e.g. a network message processor

There is indeed a long list of Requirements to Consider for Go2 Error Handling. The check/handle scheme accommodates a tiny subset of these, necessitating an awkward mix of Go1 & Go2 idioms, e.g.

handle err { return fmt.Errorf(..., err) }
v, err := f()
if err != nil {
   if isBad(err) {
      check err     // means 'throw'
   }
   // recover
}

Herein is a widely applicable approach to error handling, leveraging the C-family catch block. For the record, the author is grateful that Go does not provide C++ style exceptions, and this is not a plot to sneak them into the language through a side door :-)

The #id/catch Error Model

Let a catch identifier (catch-id) e.g. #err select a named handler. A single catch-id may appear in any assignment. A handler is known by its parameter name; the parameter can be of any type. A handler follows the catch-id(s) that trigger it and starts with catch <parameter>. Catch-ids are not variables and handler parameters are only visible within handlers, so there's no re-declaration of error variables.

These are not unique ideas. At last count, 17 posts on the feedback wiki suggest various ways to define and invoke named handlers, and 13 posts suggest invocation of handlers using assignment syntax.

func (db *Db) GetX(data []byte) (int, error) {
   n, #return := db.name()                       // return our own errors
   
   f, #err := os.Open(n)                         // this file's presence is optional
   defer f.Close()
   _, #err  = f.Seek(42, io.SeekStart)
   l, #err := f.Read(data)
   
   #return = db.process(data)
   
   catch err error {                             // handle OS errors here
      if !os.IsNotExist(err) { log.Fatal(err) }
      log.Println(n, "not found; proceding")
   }

   #return = db.index(data)                      // executes unless catch exits
   return l, nil
}

Several points are unresolved, see Open Questions below. Catch-id syntax is among them; #id is reminiscent of the URL form for goto id, but ?id, @id, and others are viable.

Advantages: similarity to established try/catch method (but without a try-block); clarity as to which handler is invoked for a given statement; certain statements may be skipped after an error occurs; handlers can return or continue the function.

Please help clarify (or fix) this proposal sketch, and describe your use cases for its features.

Feature Summary

Draft-2, 2018-09-19. Discussion following this comment below pertains to this draft.

These features meet a large subset of the Requirements to Consider for Go 2 Error Handling.

We can select one of several distinct handlers:

func f() error {
   v1, #fat := fatalIfError()     // a non-zero value for #id triggers corresponding catch
   v2, #wrt := writeIfError()

                                  // predefined handlers
   v3, #_   := ignoreIfError()    // or log the error in debug mode
   v4, #r   := returnIfError()    // aka #return
   v5, #p   := panicIfError()     // aka #panic
   
   catch fat       { log.Fatal(fat) }           // inferred parameter type
   catch wrt error { conn.Write(wrt.Error()) }
}

We can invoke a handler defined at package level (thanks @8lall0):

func f() error {
   #pkg = x()
}

catch pkg error {           // package-level handler; explicit type
   log.Println(pkg)
   return pkg               // return signature must match function invoking pkg handler
}

We can specify a type for implicit type assertion:

   f := func() error { return MyError{} }
   #err = f()
   catch err MyError { ... }

We can skip statements on error and continue after the handler:

   #err = f()
   x(1)                              // not called on error
   catch err { log.Println(err) }
   x(2)                              // always called

We can forward to a different handler (creates an explicit handler chain):

   #ret = x()
   if ... {
      #err = f()
      catch err {
         if ... { #ret = err }         // invoke alternate handler
         #ret = fmt.Errorf(..., err)   // invoke handler with alternate input
      }
   }
   catch ret { ... }

We can reuse catch-ids:

   #err = f(1)
   catch err { ... }
   #err = f(2)
   catch err { ... }

We can nest catch blocks:

   #era = f(1)
   catch era {
      #erb = f(2)
      catch erb { ... }      // cannot use 'era'; shadowing in catch disallowed
   }

We can see everything from the scope where a handler is defined, like closure functions:

   v1 := 1
   if t {
      v2 := 2
      #err = f()
      catch err { x(v1, v2) }
   }

We can still use Go1 error handling:

   v1, err := x()  // OK
   v2, err := y()  // but re-declaration might be abolished!

Open Questions

  • What catch-id syntax? #id, ?id, @id, id!, $id, ...
    What style for predefined handlers? #r, #p, #_, #return, #panic, #nil, ...

  • What handler definition syntax? catch id [type], catch id(v type), id: catch v [type], ...
    Infer parameter from previous stmt? #err = f(); catch { log.Println(err) }

  • Invoke handler when ok=false for v, #ok := m[k]|x.(T)|<-c, etc?
    Pass a type error with context? v, #err := m[k]; catch { log.Println(err) }

  • Treat parameter as const? catch err { err = nil } // compiler complains
    Lets forwarding skip test for nil: catch err { #ret = err }

  • Require #id for return values of type error? #20803

  • Provide check functionality with f#id()? e.g. x(f1#_(), f2#err())
    If so, disallow nesting? x(f1#err(f2#err()))
    Allow position selector? f#id.0() tests first return value

  • Provide more context to package-level handlers, e.g. caller name, arguments?
    catch (pkg error, caller string) { ... }

  • Allow handlers in defer stack?

     defer last()              // skip if handler returns
     defer catch errd { ... }
     defer next#errd()         // skip if first() invokes handler
     defer first#errd()
    
  • Allow multiple handler arguments?

     #val, #err = f()                 // return values assignable to catch parameter types
     catch (val T, err error) { ... } // either parameter could be non-zero
    

Disallowed Constructs

Declaring or reading a catch-id:

   var #err error           // compiler complains
   #err = f()
   if #err != nil { ... }   // compiler complains
   catch err { ... }

Multiple catch-ids per statement:

   #val, #err = f()   // compiler complains
   catch val { ... }  // if f() returns two non-zero values, which handler is executed?
   catch err { ... }

Shadowing of local variables in handlers:

func f() {
   if t {
      err := 2
      #err = f()            // OK; #err handler can't see this scope
   }
   pkg := 1                 // OK; #pkg handler (see above) can't see local variables
   err := 1
   #err = f()
   catch err { return err } // compiler complains; err==1 is shadowed
}

Self-invocation:

   #err = f(1)
   catch err {
      #err = f(2)        // compiler complains
   }
   #err = f(3)
   catch err { ... }

Unused handlers:

   catch err { ... }         // compiler complains
   #err = f()
   catch err { ... }
   #ret = f()
   catch err { return err }  // compiler complains
   catch ret { ... }
   catch ret { return ret }  // compiler complains

Discarded Ideas

Chain handlers with same catch-id in related scopes implicitly, as in the Draft Design:

func f() {
   v, #fat := x()
   if v != nice {                      // new scope
      #fat = y(&v)
      catch fat {                      // invoked 1st
         if ... { #fat = nil }         // can skip other handlers in chain
      }                                // no return/exit, continue along chain
   }
   catch fat { log.Fatal(fat) }        // invoked 2nd
}

Changelog

2018-09-19 draft-2 (discussion below)
a) Move implicit handler chain to new section "Discarded Ideas".
b) Make continuing after catch the default behavior.
c) Document catch-id reuse and nested catch block.
d) Disallow unused handlers (was "contiguous handlers with same catch-id") and self-invocation.
e) Add #_ predefined handler to ignore or log input.
f) Add implicit type assertion.

Why Not check/handle?

Please read Golang, how dare you handle my checks! for a discussion of each of the following points.

  • No support for multiple distinct handlers.
  • The last-in-first-out handle chain cannot continue a function.
  • check is specific to type error and the last return value.
  • The per-call unary check operator can foster unreadable constructions.
  • The default handler makes it trivial to return errors without context.
  • Handlers appear before the calls that trigger them, not in the order of operations.
  • The handle chain is inapparent; one must parse a function by eye to discover it.

Also, there is relatively little support for the draft design on the feedback wiki.

At last count, roughly 1/3rd of posts on the feedback wiki suggest ways to select one of several handlers:

  1. @didenko github
  2. @forstmeier gist
  3. @mcluseau gist
  4. @the-gigi gist
  5. @PeterRK gist
  6. @marlonche gist
  7. @alnkapa github
  8. @pdk medium
  9. @gregwebs gist
  10. @gooid github
  11. @spakin gist
  12. @morikuni gist
  13. @AndrewWPhillips blogspot
  14. @bserdar gist
  15. @martinrode medium
  16. @dpremus gist
  17. @networkimprov this page

And the following posts suggest ways to invoke a handler with assignment syntax:

  1. @oktalz gist
  2. @pborman gist
  3. @kd6ify blog
  4. @rockmenjack github
  5. @the-gigi gist
  6. @8lall0 gist
  7. @dpremus gist
  8. @bserdar gist
  9. @mcluseau gist
  10. @didenko github
  11. @gooid github
  12. @Kiura gist
  13. @networkimprov this page

/cc @rsc @mpvl @griesemer @ianlancetaylor @8lall0 @sdwarwick @kalexmills

Thanks for your consideration,
Liam Breck
Menlo Park, CA, USA

@gopherbot gopherbot added this to the Proposal milestone Sep 5, 2018
@gopherbot gopherbot added the Proposal label Sep 5, 2018
@networkimprov
Copy link
Author

@networkimprov networkimprov commented Sep 5, 2018

Go error handling works fine today, no new error specific syntax is necessary.

Some new general-purpose features (macros?) would ease error handling.

@PeterRK
Copy link

@PeterRK PeterRK commented Sep 6, 2018

Mix of Go1 & Go2 idioms is not a problem.

@xiaoxubeii
Copy link
Contributor

@xiaoxubeii xiaoxubeii commented Sep 6, 2018

I don't like some kind of "Error-Handle", especially:

When an error check fails, it transfers control to the innermost handler, which transfers control to the next handler above it, and so on, until a handler executes a return statement.

It is hard to find which handle will catch the error unless count the code above. And it is not possible to specify the error handle.

@gregwebs
Copy link

@gregwebs gregwebs commented Sep 7, 2018

I am surprised to see this posted here as a github issue. Can we have more transparency and guidance on the go 2 draft proposal feedback process? As it stands, we have only been directed to leave feedback on a wiki with no indication of future steps.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 7, 2018

Yes, we prefer feedback on the wiki. Thanks.

That said, this seem less like feedback on the design draft, and more like a completely different proposal.

@deanveloper
Copy link

@deanveloper deanveloper commented Sep 7, 2018

A major issue with this is the same as with gotos.

func errorProne() {
    x, #err := oops() // goes straight to `catch err error`
    i := 10 // define variable i
    
    catch err error {
        fmt.Println(i) // can't print i, as it was never defined, even though it is in-scope
    }
}

Currently, goto gets around this by saying no variables may be defined inside any region between goto lbl and lbl: (although there are a couple proposals that are refining this to make things a bit easier)

Also, what about this?

func errorProne() {
    x, #err := oops() // err not nil
    y, #err2 := oops2() // err2 not nil

    fmt.Println(0)

    catch err error {
        fmt.Println(1)
    }
    
    fmt.Println(2)

    catch err2 error {
        fmt.Println(3)
        return
    }
    
    fmt.Println(4)
}

What order do these print in? Because it looks like the program runs top-down, does it print 1 3, or 1 2 4? I think the check/handle construct made this a bit clearer since it was extremely clear that the program does not run top-down, but because in this example, since the catches are defined below the #ids, it makes it a lot more difficult to recognize the flow of the program just by looking at it.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Sep 7, 2018

What if it gets treated like a variable? catch err error would declare a new catch called err of type error, and only once it's in scope can you use it.

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Sep 7, 2018

Re goto that will be fixed: #27165

Re errorProne(), there is no check/handle analog for #err, as handle cannot continue the function. Since #err2 returns, you write its catch immediately below the call; elsewhere should be flagged. Triggering a catch-id is like a throw, so oops() failure skips oops2(). Results: ok 0,2,4; #err 1,2,4; #err2 3

N.B. in #id/catch draft 1, you need #id = nil to continue, but that may become the default next draft.

@DeedleFake, yes the handler parameter is only accessible in that handler.

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Sep 7, 2018

It is hard to find which handle will catch the error unless count the code above. And it is not possible to specify the error handle.

@xiaoxubeii, lots of people agree, see Named Handlers Are Popular above.

#id/catch draft 1 offers implicit (i.e. automatic) handler chains as in check/handle, and explicit ones where one handler forwards the error to another by name. However I believe implicit chaining should be dropped, because it is not obvious when chained handlers are far apart in a function.

@deanveloper
Copy link

@deanveloper deanveloper commented Sep 7, 2018

Re goto that will be fixed: #27165

It's not "fixed", it's just expanded upon. You can now declare variables under that construct, but you can't use them outside of the range between the goto lbl and the lbl:. My example still should not compile, even though it looks intuitive.

Since #err2 returns, you write its catch immediately below the call; elsewhere should be flagged.

Assuming you mean "flagged" as in give a compiler error (as there are no warnings in Go), would the following be illegal?

func errorProne() error {
    x, #err := oops() // err not nil
    y, #err := oops2() // err not nil
    catch err error {
        return wrapErr("error in errorProne", err)
    }
}

Since catch err returns, it needs to be placed immediately after... So since there is code between the oops() call and the catch, it should fail to compile, correct? I'm not sure I'm understanding this.

If so, then all you've done is replaced if err != nil with catch err, which is definitely not a good solution.

Re errorProne(), there is no check/handle analog for #err, as handle cannot continue the function. Since #err2 returns, you write its catch immediately below the call; elsewhere should be flagged. Triggering a catch-id is like a throw, so oops() failure skips oops2(). Results: ok 0,2,4; #err 1,2,4; #err2 3

Okay, so I understand the flow now, but I'm not sure I like it. In order to understand it, my eyes need to dart all around the function in order to follow it, when it should be going a single direction.

The nice thing about check/handle was that the function went downward as normal, but then once an error occurs, it flows (block-wise) upward.

Going back to the original draft -

func f() error {
   #pkg = x()
   catch pkg { ... } // optional
}

catch pkg {                 // package-level handler
   log.Println(pkg.Error())
   return pkg               // return signature must match function invoking pkg handler
}

I don't like this - I want my returns to be within the function... a function's flow should not be determined by something outside of the function (ever). Also, how would this work with multi-value returns?


Again, my main gripe with the #id/catch proposal is that it gives the illusion that the function is running from the top-down. The check/handle very obviously shows that it does NOT run top-down, because the handler for an error needs to be declared above the check. I do like the concept of a named error handler, but I'm not sure that this is the way to do it. (Sorry if I'm coming off as mean or something, just trying to give constructive criticism)

@gregwebs
Copy link

@gregwebs gregwebs commented Sep 7, 2018

@ianlancetaylor the majority of the feedback on the wiki would probably qualify as a new design. All of it solves the same problem overview scenario, and most contains a new control-flow operation and a technique for re-usable handlers.

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Sep 7, 2018

Re goto, guess we'll have to fix that!

Re errorProne(), see the first proposal example. What should be flagged is a returning handler placed farther away than nec. Flagged could be via go vet.

Re package-level handlers, check/handle has a default handler, which magically returns your function; #pkg is rather similar, just customized.

Sure #id/catch can skip over stmts after #id assignment and in catch blocks. If/else/break/continue also skip stmts. Not seeing where your eyes are darting :-)

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Sep 7, 2018

@ianlancetaylor, I suggest that the check/handle proposal needs its own issue. Maybe require participants to have posted on the wiki? And designate @gregwebs as a moderator there?

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 7, 2018

We don't want a single issue discussing the check/handle proposal. It might be appropriate to have issues discussing specific aspects of it, with absolutely no suggestion of new proposals on those issues. If we permit any issue discussion to introduce arbitrary new proposals for error handling, we will soon have a repeat of #21161, which simply isn't helpful. If we want to discuss new proposals in this area, each new proposal needs a separate issue.

@deanveloper
Copy link

@deanveloper deanveloper commented Sep 7, 2018

Re goto, guess we'll have to fix that!

I'm curious as to what you mean by "fix"... the original statement was

func errorProne() {
    x, #err := oops() // goes straight to `catch err error`
    i := 10 // define variable i
    
    catch err error {
        fmt.Println(i) // can't print i, as it was never defined, even though it is in-scope
    }
}

There's no way to "fix" this. It can't compile (without panics at runtime), even though all variables are in scope. It just adds more complexity.

Sure #id/catch can skip over stmts after #id assignment and in catch blocks. If/else/break/continue also skip stmts. Not seeing where your eyes are darting :-)

Because they still flow top-to-bottom... They skip code then come back to it later. Let's look at your first example:

func (db *Db) GetX(data []byte) (int, error) {
   n, #_   := db.name()                          // return our own errors via default handler
   
   f, #err := os.Open(n)                         // this file's presence is optional
   defer f.Close()
   _, #err  = f.Seek(42, io.SeekStart)
   l, #err := f.Read(data)
   
   #_ = db.process(data)
   
   catch err error {                             // handle OS errors here
      if !os.IsNotExist(err) { log.Fatal(err) }
      log.Println(n, "not found; proceding")
      #err = nil                                 // resume (should be the default?)
   }
   return l, nil
}

Let's assume the #_ are ok, and that each #err results in an error.

If I want to trace how this runs...

here ya go

I feel that the reason check/handle system doesn't allow to continue execution is because it makes the flow of a function extremely hard to follow. (If it did allow for this, it would be a disaster).

If/else/break/continue also skip stmts...
Of course they do, but they skip statements in a way that is easy to read.

if false {
    // all indented stuff is skipped until the next }
} else {
    // this indented stuff gets ran instead
}
for i := range arr {
    if condition {
        // everything after this gets skipped, we go to the top of the for loop.
        // this is clear to find because everything inside the for loop is indented.
        continue
    }
    if otherCondition {
        // everything in the for loop (which is indented) is skipped
        break
    }
}
@networkimprov
Copy link
Author

@networkimprov networkimprov commented Sep 7, 2018

Re goto, i==0, but go vet should maybe flag it. Compiler error works too.

The trace diagram is delightful but inaccurate. "Resume" means continue execution after the catch block. Which is how catch works in other languages. I'll clarify the Feature Summary on that point. (But note that the behavior you imagined is similar to using a locally-defined function :-)

@deanveloper
Copy link

@deanveloper deanveloper commented Sep 8, 2018

func errorProne() {
    x, #err := oops() // err not nil
    y, #err2 := oops2() // err2 not nil

    fmt.Println(0)

    catch err error {
        fmt.Println(1)
    }
    
    fmt.Println(2)

    catch err2 error {
        fmt.Println(3)
        return
    }
    
    fmt.Println(4)
}

So this works like a locally defined function (minus having a return inside of the catch)

func (db *Db) GetX(data []byte) (int, error) {
   n, #_   := db.name()                          // return our own errors via default handler
   
   f, #err := os.Open(n)                         // this file's presence is optional
   defer f.Close()
   _, #err  = f.Seek(42, io.SeekStart)
   l, #err := f.Read(data)
   
   #_ = db.process(data)
   
   catch err error {                             // handle OS errors here
      if !os.IsNotExist(err) { log.Fatal(err) }
      log.Println(n, "not found; proceding")
      #err = nil                                 // resume (should be the default?)
   }
   return l, nil
}

But this doesn't because of the #err = nil? (which seems more of a syntax hack to resume rather than a clear and concise way)

Do you see my confusion here? Do you see why I'm a being a bit critical?

I've discussed most of the points about this proposal already, so I think I'm going to stop here. I just think it's hard to read and hard to follow, and it just doesn't feel like Go to me.

I do like the idea of having named error handlers, though!

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Sep 8, 2018

The two cases do not work differently. See my N.B. (Latin for "note well, dear reader") above.
catch err error { fmt.Println(1) } would not compile per draft 1.

Agreed that #id = nil is not an obvious way to continue; as mentioned, next draft will change that.

@PeterRK
Copy link

@PeterRK PeterRK commented Sep 8, 2018

@networkimprov networkimprov changed the title proposal: Go 2: The #id/catch error model, a revision of check/handle proposal: Go 2: The #id/catch error model, a rethink of check/handle Sep 9, 2018
@networkimprov
Copy link
Author

@networkimprov networkimprov commented Sep 19, 2018

Draft-2 complete, as of 2018-09-19.

Discussion above pertains to draft-1.

@odiferousmint
Copy link

@odiferousmint odiferousmint commented Feb 18, 2019

This introduces a lot of complexity unnecessarily. It is definitely not Go's way. This is not to say that I agree with the check one either, since all it does is change the order when there are very different statements you would like to execute, for example:

var err error

err = foo()
if err != nil {
  f()
}

err = bar()
if err != nil {
  g()
}

which would become:

handle err {
  f()
}
catch foo()

handle err {
  g()
  return
}
catch bar()

How the latter is any better, I don't know.

And the return in the last example is required otherwise f() would get executed which is not the case in the first example. I prefer the first version, it is clean, minimal, and allows you to glance over it quickly without any consideration, and when writing the code it is also extremely intuitive as to what is going to happen vs. handler chains.

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Feb 18, 2019

I encourage you to paste your critique of check/handle into a gist and list it on the feedback wiki.

We agree that if err != nil {...} is effective in many cases. But when constantly calling an API where most calls yield error (e.g. os.File), the 3 lines of boilerplate for every call decreases the program's density to the point where it's inefficient for the reader; code density matters. The Go team has decided to seek a fix for this.

Given that decision, we need the set of requirements for any solution. The Go team has not compiled one, but I have in Requirements to Consider for Go2 Error Handling. All the terms therein may not be essential, but virtually all of them are satisfied by two features:

  1. #name - invoke a handler by name
  2. catch name {...} - define a handler

That's pretty easy to explain, and far easier to read when error results fall from most lines in a function :-)

@PxyUp
Copy link

@PxyUp PxyUp commented Feb 19, 2019

I like proposal with:

  • #name - invoke a handler by name
  • catch name {...} - define a handler

It solved many situations, but i think can be improved with panic and return state, like in proposal:

func returnError() error {
    return errors.New()
}

#*   := returnError()    // alias return error
#!   := returnError()    // alias panic(error)

Because it is will be decreased user code(user need write this manual) in all situations, and code will be clear and understandable, and this will solve error tracing

@valdemarpavesi
Copy link

@valdemarpavesi valdemarpavesi commented Feb 28, 2019

hello,

Go handle errors the perfect way.

it is 100% clear.

zero problems with

if err != nil {
panic(err)
}

regards!
Valdemar

@Alpt
Copy link

@Alpt Alpt commented Jun 5, 2020

I would not introduce special symbols in the language. #err, !err, ?err, ...

Go marks exported fields and functions with capital letters (log.Fatal is exported, log.innerFuction is not exported).

This would be a nicer way to go:

func f() error {
   v1, Fatal := fatalIfError()  // a non-zero value for #id triggers corresponding catch
   v2, Stderr := writeIfError()
   v3, Stderr := writeIfError()

   // predefined handlers
   v4, _ := ignoreIfError()  // or log the error in debug mode
   v5, Ret := returnIfError()  // aka #return
   v6, Panic := panicIfError()  // aka #panic
   
   catch Fatal { log.Fatal(fat) }  // inferred parameter type
   catch Stderr error { conn.Write(wrt.Error()) }
}

In other words, if the variable is "fatal", "ret", "stderr", ... and starts with an upper case letter, then it is an #err id.

Surely this idea needs more work, but I believe the point of not introducing symbols (non word characters) in the language is important.

@mcluseau
Copy link

@mcluseau mcluseau commented Jun 5, 2020

personnaly, I prefer having a clear keyword instead of increasing the semantic load on something else. The check keyword appeared many times, inline sgtm too. My preferred way is

func f() (interface{}, error) {
    check err error {
      if err != nil {
        return nil, err
      }
    }

    result, check err := callThanMayFail()
    return result, nil
}
@didenko
Copy link

@didenko didenko commented Jun 6, 2020

A feeling keeps coming back that many proposals, this included, are not abstract enough. There are attempts to address either specific value types, or a limited number of return values (commonly 1), or reuse/fit concepts from other ecosystems. But errors and returned errors among multiple values are just values and collections of values. I am contending that we are trying hard to address a narrow case of a generic problem and are failing to cover all nuances which may otherwise derive from broader principles. So let's have a proposal which does not even mention errors - but addresses them as just one application of general improvement.

To abstract it out (slide 7 in the deck you linked), we are looking to have:

  • a generic mechanism
  • to trigger a piece of code
  • each time a variable gets assigned to
  • regardless if in a single-value or a multi-value context
  • regardless of where the value comes from (func or something else)

Any modification of the language will bring a pile of corner cases to consider. Any modification will bear a whole new pattern of the language use, that is the purpose. And, yes, a more generic line of thinking may bear "more different" use patterns, and it may be scary, but it may also allow for fewer corner cases. I also encourage to consider speaking in more abstract terms than ideas of throwing and catching, which carry a cognitive load from other languages.

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Jun 6, 2020

Hi Vlad, thanks for the feedback. Note that this comment appears in the first Feature Summary example:

// a non-zero value for #id triggers corresponding catch

So yes, a single #id parameter of any type can appear in any assignment.

#x, v = 1, f()  // calls f() and invokes the catch
catch x {...}

@Alpt & @mcluseau, I'd be glad to see any variation of this scheme adopted, regardless of the assignment syntax. So yes, your variations are worth considering!

@didenko
Copy link

@didenko didenko commented Jun 6, 2020

Hi Liam, yes, I understand that this proposal is for "handlers" (for a lack of an agreed term) of a single value of any type. It is for a single value though. As I mentioned, that is a strange (to me) limitation. That does not allow for a parametrized "handler" so that it can get more information. It does not allow for a single "handler" to process multiple values if returned by f().

Philosophically I would like any number of "bindable" places on an assignment's left to belong to "handler" calls - and maybe the rest assigned to a variadic thingy. So in a synthetic case of f() returning six values, with variables v11 .. vNM, and handlers aX, bY either of those may be OK given the functions have right data type signatures:

v11, v12, a1(a11,a12, a13), b1(b11) := f() // a1 is ternary and b1 is unary, the order of call is from right to left
a2(a21), b2(b21, b22), v21... := f() // a2 unary, b2 binary, v21 gets a 3-long slice
// etc

Essentially you can think of what is on the left of the assignment in a similar way to what is inside the parenthesis of a function call. It is a very familiar concept and neither people do nor tooling has issues with it.

It is strange for me to have a special shortcut definition syntax for such "handlers". Mimicking function definitions with another keyword instead of func is simple enough.

I am not saying that this is THE solution. I am saying that we are thinking at a wrong abstraction level. And even if single-value catch addresses some of what we want to do with values, it still creates a special case of control flow in a program, compared to other constructs.

It also signals back to languages where there are try/catch concepts (which I abhor) and will likely cause significant adoption confusion - if not design confusion.

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Jun 6, 2020

Any other variables set by the assignment that invokes the handler would be visible in the catch block. If you don't like catch blocks, well, I dunno what else to say :-)

@mcluseau
Copy link

@mcluseau mcluseau commented Jun 19, 2020

@networkimprov I think we're pretty close indeed in our intentions: it's about going through inlined code common to a block or func. I initialy used handle instead of check, but it could also be inline; I don't think catch is the right name because it implies existence. What I also this is critical is to have a variable name and a type, so the inlined code is crystal clear and all type checks can be done. I also think it should be pre-defined not post-defined as in your example. [edit] I fix my previous example; I also want to node that check in ..., check err := is not mandatory but the community may prefer the explicit way.

@didenko the problem with a more abstract concept is that the number potential of miss-use cases increases exponentially.

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Jun 19, 2020

Mikaël, my concept is a feature that anyone who's written catch blocks in Java, C++, and Python can recognize. It's only difference is that try and throw are implicit in #err = ...

The variation v, try err = f() makes the try explicit, but to me a keyword in assignment looks awkward.

@mcluseau
Copy link

@mcluseau mcluseau commented Jun 19, 2020

@networkimprov I agree on the keywords in assignment, that was a suggestion I kept by default but it's totally not mandatory to have it, as I explained in my edit (if you only read e-mails you may have missed that ^^). We could also use a err() instead of # as would underscore the code execution logic behind.
I maintain that we implictly catch something and that I'd prefer a better keyword, and I don't think "others do it like that" is the right argument to convince the golang community :)

@didenko
Copy link

@didenko didenko commented Jun 22, 2020

@didenko the problem with a more abstract concept is that the number potential of miss-use cases increases exponentially.

Of course, it does - especially if "misuse" is defined in the boundaries of narrow error handling patterns.

My whole point is that error handling is pointing to a need for a significantly different flow mechanism conceptually than what is available in mainstream languages. You catch all the "catch" hate when you copy "catch" from other languages, so to speak. But if you define a new legitimate way of control flow, which also allows you to handle errors as one of its applications, then other uses are no longer "misuse" necessarily. They may be a good or a bad practice as code design is concerned, but not a misuse as "used not like it was intended". Because the intentions are not narrowly bound.

Anyhow, I shall not post here more.

@mcluseau
Copy link

@mcluseau mcluseau commented Jun 22, 2020

@didenko I understand your point, I personnally am in the "common code triggered by assignment in the function scope" line, were error handling is a particular case too, at seems not so far from yours. #id,catch is not that far too. So, I feel like we're orbiting around something, and there's a path for convergence were some kind of reasonable breakthrough may lie. Maybe, we should try to setup a meeting to sketch that path?

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Jul 28, 2020

@ianlancetaylor would you be willing to include this (and perhaps others offering local error handlers) in #40432?

Also the issue could note that a handful of proposals remain open; i.e. not declined.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 6, 2020

@networkimprov I didn't notice a general theme of proposals with local error handlers, but I could easily have missed it. Can you point me at a few others?

I don't think the meta issue needs to point out which proposals are open or closed, that will just require more updating. The information is inherently available in the issues themselves.

@networkimprov
Copy link
Author

@networkimprov networkimprov commented Aug 6, 2020

For proposals with local error handlers, see the item on this wiki page labeled "Invoke one of several handlers by name". They're mostly not on Github, because the check/handle document specifically requested feedback via the wiki.

https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes

In any case, could you add this proposal (#27519) to #40432?

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 10, 2020

OK, done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.