[RFC] Yet another proposal for Go2 error handling

1,038 views
Skip to first unread message

Shulhan

unread,
Jun 4, 2023, 4:17:17 PM6/4/23
to golang-nuts
Dear gophers,

I have been reading several proposals about error handling couple of
months ago and today a light bulb come upon me, and then I write as much
as I can think. I am not sure if its good or bad idea or even possible
to implement.

In this post, I will try as concise as possible.
The full and up to date proposal is available at
https://kilabit.info/journal/2023/go2_error_handling/ .

Any feedback are welcome so I can see if this can move forward.
Thanks in advance.

== Background

This proposal is based on "go2draft Error Handling".

My critics to "go2draft Error Handling" is the missing correlation
between handle and check.
If we see one of the first code in the design,

----
...
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

r := check os.Open(src)
...
----

There is no explicit link between check keyword and how it will trigger
handle err later.
It is also break the contract between the signature of os.Open, that
return an error in the second parameter, and the code that call it.

This proposal try to make the link between them clear and keep the code
flow explicit and readable.

The goals is not to reduce number of lines but to minimize repetitive
error handling.


== Proposal

This proposal introduces two new keywords and one new syntax for
statement.

The two new keywords are “WHEN” and “HANDLE”.

----
When = "when" NonZeroValueStmt HandleCallStmt .
NonZeroValueStmt = ExpressionStmt
; I am not quite sure how to express non-zero value
; expression here, so I will describe it below.

HandleCallStmt = "handle" ( HandleName | "{" SimpleStmt "}" ) .
HandleName = identifier .
----

The HandleCallStmt will be executed if only if the statement in
NonZeroValueStmt returned non-zero value of its type.
For example, given the following variable declarations,

----
var (
err = errors.New(`error`)
slice = make([]byte, 1)
no1 = 1

no2 int
ok bool
)
----

The result of when evaluation are below,

----
when err // true, non-zero value of type error.
when len(slice) == 0 // true, non-zero value of type bool.
when no1 // true, non-zero value of type int.
when no2 // false, zero value of int.
when ok // false, zero value of bool.
----

The HandleCallStmt can jump to handle by passing handle name or provide
simple statement directly.
If its simple statement, there should be no variable shadowing happen
inside them.

Example of calling handle by name,

----
...
when err handle myErrorHandle

:myErrorHandle:
return err
----

Example of calling handle using simple statement,

----
...
when err handle { return err }
----

The new syntax for statement is to declare label for handle and its body,

----
HandleStmt = ":" HandleName ":" [SimpleStmt] [ReturnStmt | HandleCallStmt] .
----

Each of `HandleStmt` MUST be declared at the bottom of function block.
An `HandleStmt` can call other `HandleStmt` as long as the handle is above the
current handle and it is not itself.
Any statements below `HandleCallStmt` MUST not be executed.

Unlike goto, each `HandleStmt` is independent on each other, one `HandleStmt`
end on itself, either by calling `return` or `handle`, or by other
`HandleStmt` and does not fallthrough below it.

Given the list of handle below,

----
:handle1:
S0
S1
:handle2:
handle handle1
S3
----

A `handle1` cannot call `handle2` because its below it.
A `handle2` cannot call `handle2`, because its the same handle.
A `handle2` can call `handle1`.
The `handle1` execution stop at statement `S1`, not fallthrough below it.
The `handle2` execution stop at statement "`handle handle1`", any statements
below it will not be executed.


The following function show an example of using this proposed error handling.
Note that the handlers are defined several times here for showing the
possible cases on how it can be used, the actual handlers probably only two or
three.

----
func ImportToDatabase(db *sql.DB, file string) (error) {
when len(file) == 0 handle invalidInput

f, err := os.Open(file)
when err handle fileOpen
// Adding `== nil` is OPTIONAL, the WHEN operation check for NON zero
// value of returned function or instance.

data, err := parse(f)
when err handle parseError

err = f.Close()
// Inline error handle.
when err handle { return fmt.Errorf(`%s: %w`, file, err) }

tx, err := db.Begin()
when err handle databaseError

// One can join the statement with when using ';'.
err = doSomething(tx, data); when err handle databaseError

err = tx.Commit()
when err handle databaseCommitError

var outofscope string
_ = outofscope

// The function body stop here if its not expecting RETURN, otherwise
// explicit RETURN must be declared.

return nil

:invalidInput:
// If the function expect RETURN, the compiler will reject and return
// an error indicating missing return.

:fileOpen:
// All the instances of variables declared in function body until this
// handler called is visible, similar to goto.
return fmt.Errorf(`failed to open %s: %w`, file, err)

:parseError:
errClose := f.Close()
when errClose handle { err = wrapError(err, errClose) }

// The value of err instance in this scope become value returned by
// wrapError, no shadowing on statement inside inline handle.
return fmt.Errorf(`invalid file data: %s: %w`, file, err)

:databaseError:
_ = db.Rollback()
// Accessing variable below the scope of handler will not compilable,
// similar to goto.
fmt.Println(outofscope)
return fmt.Errorf(`database operation failed: %w`, err)

:databaseCommitError:
// A handle can call another handle as long as its above the current
// handle.
// Any statements below it will not be executed.
handle databaseError

RETURN nil // This statement will never be reached.
}
----

That's it. What do you guys think?

Ian Lance Taylor

unread,
Jun 4, 2023, 7:36:56 PM6/4/23
to Shulhan, golang-nuts
On Sun, Jun 4, 2023 at 9:17 AM Shulhan <m.sh...@gmail.com> wrote:
>
> I have been reading several proposals about error handling couple of
> months ago and today a light bulb come upon me, and then I write as much
> as I can think. I am not sure if its good or bad idea or even possible
> to implement.
>
> In this post, I will try as concise as possible.
> The full and up to date proposal is available at
> https://kilabit.info/journal/2023/go2_error_handling/ .
>
> Any feedback are welcome so I can see if this can move forward.

Thanks. Perhaps I misunderstand, but this seems to provide a
different way of writing an if statement and a goto statement.
Instead of writing

if err != nil {
goto parseError
}

I can write

when err handle parseError

Any change to error handling is going to affect all Go code
everywhere. If we change anything it needs to be a big improvement.
It's not worth changing all Go code everywhere for a small
improvement. After all, Go does work OK today for most people. It's
not clear to me that this proposal is a big enough improvement.
Thanks.

Ian

Justin Israel

unread,
Jun 5, 2023, 12:28:30 AM6/5/23
to golang-nuts
I don't really understand the comparison between this proposal and the referenced previous one. 
This new proposal effectively makes you have to handle errors for every call site, just like we do now, but with the indirect flow of jumping to a new section of code. And it requires 2 new keywords and new label syntax to achieve it. Could we not replicate this behavior the same way with nested local scope functions as the handlers and just call them with normal if logic? 

Jan

unread,
Jun 5, 2023, 8:06:37 AM6/5/23
to golang-nuts
Repeating Justin's consideration: one of my (and from colleagues I discuss the topic with) major issues with current error handling is the repetition of identical code. Your proposal still requires `when err handle ...` at every statement. It also doesn't allow for nested call of functions that return errors -- e.g: `f(g(h(x)))`, where `h`, `g`, and `f` can return errors (all presumably handled the same way).

Shulhan

unread,
Jun 17, 2023, 6:47:27 AM6/17/23
to Ian Lance Taylor, golang-nuts
On Sun, 4 Jun 2023 12:36:20 -0700
Ian Lance Taylor <ia...@golang.org> wrote:

> On Sun, Jun 4, 2023 at 9:17 AM Shulhan <m.sh...@gmail.com> wrote:
> >
> > I have been reading several proposals about error handling couple of
> > months ago and today a light bulb come upon me, and then I write as
> > much as I can think. I am not sure if its good or bad idea or even
> > possible to implement.
> >
> > In this post, I will try as concise as possible.
> > The full and up to date proposal is available at
> > https://kilabit.info/journal/2023/go2_error_handling/ .
> >
> > Any feedback are welcome so I can see if this can move forward.
>
> Thanks. Perhaps I misunderstand, but this seems to provide a
> different way of writing an if statement and a goto statement.
> Instead of writing
>
> if err != nil {
> goto parseError
> }
>
> I can write
>
> when err handle parseError
>

In some sense yes, it is identical to writing and combining if and goto
statements.
The semantic of `when` different on the condition that they evaluate.

The `if` statement only continue if expression evaluate to true, the
`when` condition only continue if the expression evaluate to non-zero
value.
So, the "when" statement provide a connection with the handle label.

The goto imply that the statements before it fall-through after it.
For example,

S0
goto1:
S1
goto2:
S2

Statement S1 will be executed after S0, statement S2 will be executed
after S1.
Case in example, the following goto example will loop forever,

if true { goto goto2 }

goto1:
println("goto1")
goto2:
println("goto2")
goto goto1


While handle ":name:" scope only on that handle body.

The following properties describes how handle and its label are
different with goto and its label,

* Each of `HandleStmt` MUST be declared at the bottom of function
block.
* An `HandleStmt` can call other `HandleStmt` as long as the handle is
above the current handle and it is not itself.
* Any statements below `HandleCallStmt` MUST not be executed.
* Unlike goto, each `HandleStmt` is independent on each other, one
`HandleStmt` end on itself, either by calling `return` or `handle`,
or by other `HandleStmt` and does not fall-through below it.

For example,

S0
:handle1:
S1
:handle2:
S2

Statement S0 stop and never fall-through :handle1:.
Statement S1 stop and never fall-through :handle2:.
Case in example, the following handle will not end with infinite loop,

...
when err handle handle2

:handle1:
println("handle1")
:handle2:
println("handle2")
handle handle1


> Any change to error handling is going to affect all Go code
> everywhere. If we change anything it needs to be a big improvement.
> It's not worth changing all Go code everywhere for a small
> improvement. After all, Go does work OK today for most people.

Agree with that.

The original purpose of this proposal in the Abstract [1] is that its
not only can be use for handling error but also for handling other
control flow.

The goals is not to reduce number of lines but to minimise repetitive
error handling.

> It's
> not clear to me that this proposal is a big enough improvement.
> Thanks.

I am shooting in the dark here and see if it make sense from other
perspective.
Thanks for reviewing.

--
[1] https://kilabit.info/journal/2023/go2_error_handling/#abstract

Shulhan

unread,
Jun 17, 2023, 7:00:01 AM6/17/23
to Justin Israel, golang-nuts
Hi Justin, thanks for reviewing, sorry for late response.

On Sun, 4 Jun 2023 17:28:29 -0700 (PDT)
Justin Israel <justin...@gmail.com> wrote:

>
> I don't really understand the comparison between this proposal and
> the referenced previous one.

If we see one of the first code in the previous proposal design,

...
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

r := check os.Open(src)
...

There is no explicit link between the "check" keyword and how it will
trigger "handle err" later.

It is also break the contract between the signature of "os.Open", that
return an error in the second parameter and the code that call it.

In other words, if someone new to the code, we need to explain to them
that os.Open _actually_ return an error as second return value, and
that is being checked by "check" using handle.

This proposal try to make the link between them clear and
keep the code flow explicit and readable, I think.


> This new proposal effectively makes you have to handle errors for
> every call site, just like we do now, but with the indirect flow of
> jumping to a new section of code. And it requires 2 new keywords and
> new label syntax to achieve it. Could we not replicate this behavior
> the same way with nested local scope functions as the handlers and
> just call them with normal if logic?
>

Some disadvantages of using function to handle error are, first, its
expect the call to the function pass the required parameters and handle
the returned value back by the caller.

Second, the context between the error to be handled and function to be
called can be far away (the error function may be defined in different
file or outside of current package).

When using HandleStmt all required variables that cause the errors and
the error itself is in the same scope, there is no flow break between
the cause and handler, except the jump that you mentioned above.

Shulhan

unread,
Jun 17, 2023, 7:22:10 AM6/17/23
to Jan, golang-nuts
Hi Jan, thanks for response.

On Mon, 5 Jun 2023 01:06:37 -0700 (PDT)
Jan <pfe...@gmail.com> wrote:

> Repeating Justin's consideration: one of my (and from colleagues I
> discuss the topic with) major issues with current error handling is
> the repetition of identical code. Your proposal still requires `when
> err handle ...` at every statement.

Yes, correct.
The idea is not to minimise repetition on error handling statement, the
"if err != nil", but to minimise repetitive error handling body by
grouping it into single handle based on the context of error.

> It also doesn't allow for nested
> call of functions that return errors -- e.g: `f(g(h(x)))`, where `h`,
> `g`, and `f` can return errors (all presumably handled the same way).
>

I am not sure I understand this, but should not each function handle it
on its own?
Or should not `f` being called if `g` return an error?
If its later, yes, it does not allow for nested handling of error.

Jan Pfeifer

unread,
Jun 18, 2023, 2:47:40 PM6/18/23
to Shulhan, golang-nuts
hi Shulhan, I see your points, thanks for the reply. Let me comment on them below:

On Sat, Jun 17, 2023 at 9:21 AM Shulhan <m.sh...@gmail.com> wrote:
Hi Jan, thanks for response.

On Mon, 5 Jun 2023 01:06:37 -0700 (PDT)
Jan <pfe...@gmail.com> wrote:

> Repeating Justin's consideration: one of my (and from colleagues I
> discuss the topic with) major issues with current error handling is
> the repetition of identical code. Your proposal still requires `when
> err handle ...` at every statement.

Yes, correct.
The idea is not to minimise repetition on error handling statement, the
"if err != nil", but to minimise repetitive error handling body by
grouping it into single handle based on the context of error.

Oh, I see. I have to say I wish one didn't need to do the `if err != nil` all the time either, when the handling is the same :) But if that is your proposal it makes sense.

> It also doesn't allow for nested
> call of functions that return errors -- e.g: `f(g(h(x)))`, where `h`,
> `g`, and `f` can return errors (all presumably handled the same way).
>

I am not sure I understand this, but should not each function handle it
on its own?
Or should not `f` being called if `g` return an error?
If its later, yes, it does not allow for nested handling of error.

Sorry I was not clear. What I mean is, with current error handling would be written like:

hValue, err := h(x)
if err != nil {...}
gValue, err := g(hValue)
if err != nil {...}
fValue, err := f(gValue)
if err != nil {...}

Where all of the `if err` are identical.

Let me provide a more concrete example: I'm working on an ML framework (github.com/gomlx/gomlx), and I'm writing computational graphs (that get just-in-time compiled for speed/GPU support) like:

func EuclideanDistance(x, y *Node) *Node {
   return Sqrt(ReduceAllSum(Square(Sub(x, y))))
}

Where each of these functions could return an error (if shapes are incompatible). If an error is raised (they hold a stack trace), I want them simply propagated upwards. Error handling in the middle of these math expressions (some are larger) get in the way of the math being expressed.

cheers 
Jan


 

Robert Engels

unread,
Jun 18, 2023, 3:11:45 PM6/18/23
to Jan Pfeifer, Shulhan, golang-nuts
Seems  the easiest way to address that is with a DSL and a code generator. 

On Jun 18, 2023, at 9:47 AM, Jan Pfeifer <pfe...@gmail.com> wrote:


--
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 golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAE%3D7LsUQfPLP1w0s9EN3XZSsoproEStkQHRb-s8pcdTgmpbzTw%40mail.gmail.com.

Jan Pfeifer

unread,
Jun 18, 2023, 8:10:53 PM6/18/23
to Robert Engels, Shulhan, golang-nuts
A DSL + code generator is a longer discussion. But I'm not sure I'm getting the scope of your suggestion right:

If you refer to having a full ML language DSL and a generator/AOT(ahead-of-time) compiler: folks have tried it before -- see the Swift ML effort by Chris Lattner, now leading Mojo in Modular AI. I don't know the detailed story there, but I thought hard about this (having a separate ML language and generator) before I started the GoMLX project. But from what I can imagine some of the shapes (and hyperparameters) that need checking often are only known at runtime. Also for development and debugging it complicates things: it's super nice to have a full language (Go) in parallel to constructing the graph. 

If you are referring to a DSL and code generator for error handling only: I'm not sure it would work well because it requires an extra step when developing ... it is just not as convenient / portable -- for instance for IDEs; also I'm often coding in a Jupyter Notebook using GoNB, which would also need modifications. Better if there was some form of support for the language. I'm hoping some error handling proposal would cover this use case.

Robert Engels

unread,
Jun 19, 2023, 4:36:28 AM6/19/23
to Jan Pfeifer, Shulhan, golang-nuts
What I was suggesting was a far narrower scope. I can see how go’s error handling makes writing composable math functions more difficult (although I would think these would panic not return an error - so then it is trivial). 

If they do need to return errors then using a DSL for the equations and go generate you don’t really care how much boilerplate code there is - you don’t read or edit it. 

On Jun 18, 2023, at 3:10 PM, Jan Pfeifer <pfe...@gmail.com> wrote:



Jan

unread,
Jun 26, 2023, 8:36:39 AM6/26/23
to golang-nuts
Thanks for the clarification Robert.

In many cases, when building ML models, panicking is fine. But in some, it is not, specially if running some type of server, that will handle different models, or with various shapes of inputs (e.g. images in different sizes and formats, some may lead to unexpected invalid combinations, that cannot crash a server).

Now using a DSL and go generate seems not ergonomic ? I understand it wouldn't play well with IDEs (auto-complete, info about the parameters of function one is calling, etc.), and other various checks we get for granted. Or am I missing something? Care to give a small example of what you envision ? 

Notice these math functions are inter-spaced with normal Go code. Some recent example code here (an example diffusion model, that generates flower images)  for those curious.
Message has been deleted

Sven Anderson

unread,
Jun 28, 2023, 5:30:41 PM6/28/23
to Shulhan, golang-nuts
I think for what you want to do you don't need any language extension. You can implement all that within the current language. Look at this example: 

If you want to catch errors also in nested functions, you can also skip the ErrorChecker type and do it like this: 

If you don't like the named return parameter, you can use a wrapper function:

I thought about putting something like that in a package on github. But it is such a small code, that I think copy and paste is just fine. :-)

Cheers


--
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 golang-nuts...@googlegroups.com.

Jan

unread,
Jun 29, 2023, 5:56:16 AM6/29/23
to golang-nuts
Thanks for the pointer Sven!

I think I was once told not to use panic/defer/recover to implement generic exceptions in Go, and never reconsidered it. I think I'm taking up on your suggestion for my library.

Mike Schinkel

unread,
Jun 30, 2023, 5:04:01 AM6/30/23
to golang-nuts
There are many aspects of this proposal that I really like, and a few that I think need to be reconsidered.

In general I like that it:
  1. Does not try to handle errors above where the errors occurred, which is the pattern when using both defer and recover(). I personally find having to jump back much harder to reason about than jumping forward to find the error handler.
  2. Does not require handling the error in a closure or other func as the former adds a lot of visual noise and the latter complicates by creating a different variable scope. 

It is in many ways similar to a proposal I have been planning on preparing but have no found the right opportunity. Possibly this is the right opportunity and maybe taking aspects of this and aspects of mine we could find a solution beneficial enough that Ian Lance Taylor would consider it a big enough improvement. I'll try to follow up on that soon.

As for the specifics of this proposal:

1. I think when err handle <label> is a bit too much magic.  I think it would be more Go-like if it were instead:

   when err!=nil handle <label>

I know this makes for more repeated syntax, but within the scope of this proposal I do not think you can avoid it without it adding magic.  I do think that could be addressed, and what I have in mind addresses that. But I wanted to first comment on your proposal, not introduce mine.

2. As for check as you have proposed it — handling zero return values — it seems to add more magic, and that it would only work in selected use-cases so it might be best to omit this from the proposal?

3. I do like the concept of labels that do not fall through to the code in the next label.  However, it is not exactly clear how they should work.  Do your handlers always have to have a return themselves?

   func foo()error {
     ...
     when err != nil handle cleanup
     ...
     return nil

   :cleanup:
     doCleanup()
     return err
   }

4. When you are in a handler, is there any way to correct the problem and resume or retry the code where you left off without resorting to using goto and labels?

5. I do not think you need the new keyword handle, i.e. I think this would be sufficient to use goto instead since the syntax of your handler label with the preceding colon should be sufficient to distinguish between the two:

   when err!=nil goto <label>

6. If you use goto instead of handle then you actually have two orthogonal features, the latter one being labels that do not fall through that can be used without when. Those could be proposed as a feature in their own right.

7. How would your proposal handle shared cleanup?  As we can see from the following example I have to call db.Rollback() for every error case, and I would really rather only have to call it in one place in the case.  Does your proposal have a solution for this that I missed?

   func (db *Database) Transfer(from, to Account, amount int) (err error) {
     err = db.Begin()
     when err != nil goto noTrans
     
if from.Balance() >= amount {
      goto noFunds
     }
     
err = from.Withdraw(amount)
     when err != nil goto noWithdraw
     
err = to.Deposit(amount)
     when err != nil goto noDeposit
     err = db.Commit()
     when err != nil goto noCommit
     
return nil
     :noCommit:
       db.Rollback()
       return fmt.Errorf("cannot commit; %w", err)
     :noDeposit:
       db.Rollback()
       return fmt.Errorf("cannot deposit; %w", err)
     :noWithdraw:
       db.Rollback()
       return fmt.Errorf("cannot withdraw; %w", err)
     :noTrans:
       db.Rollback()
       return fmt.Errorf("cannot begin transaction; %w", err)
     :noFunds:
       db.Rollback()
       return errors.New("no funds")
     } 8. Using the example above, is there not a way to also annotate the error in a shared manner vs. having to have all the different handle labels and duplicated code?

-Mike

Sven Anderson

unread,
Jul 1, 2023, 12:42:14 PM7/1/23
to golang-nuts

Mike Schinkel <mi...@newclarity.net> schrieb am Fr. 30. Juni 2023 um 07:04:

when err!=nil goto <label> 

What is the difference to `if err != nil { goto <label> }` ?

Go is awesome, because it does _not_ more and more syntax, especially with subtle nuances that one has to remember.

Mike Schinkel

unread,
Jul 2, 2023, 5:34:40 AM7/2/23
to golang-nuts
Hi Sven,


> What is the difference to if err != nil { goto <label> } ?

Thanks you for asking.

If you run go fmt  on a file that contains the formatting you ask about the line will be expanded to the 3 lines, which brings us back to status quo:

if err != nil {
goto <label>
}
Of course go fmt could be modified, but allowing that formatting strikes me as even more non-Go idiomatic than added a when command.
That said, I am not personally advocating for a when command, I was just comparing when err != nil goto <label> with the OP and proposer's when err handle <label>. #fwiw -Mike P.S. I think there is a approach similar to that proposed that simplify error handling even more as it would not require any code where this proposal requires when — a proposal that could also kill another bird with the one stone — but as the tendency of online programming communities is to shoot down proposals that present new ideas before their benefits can be fully understood I have been trying to find a way to introduce the idea without it falling victim to that fate.


On Saturday, July 1, 2023 at 8:42:14 AM UTC-4 Sven Anderson wrote:

Dan Kortschak

unread,
Jul 2, 2023, 5:55:31 AM7/2/23
to golan...@googlegroups.com
On Sat, 2023-07-01 at 22:34 -0700, Mike Schinkel wrote:
> > What is the difference to if err != nil { goto <label> } ?
>
> Thanks you for asking.
>
> If you run go fmt  on a file that contains the formatting you ask
> about the line will be expanded to the 3 lines, which brings us back
> to status quo:
>
>    if err != nil {
>      goto <label>
>    }
>

Why is there a bias towards favouring horizontal code over vertical
code?

In my experience, it's much easier to read and digest vertical code
compared to horizontal; Java (horribly horizontal in general) cf Go
(generally quite vertical).


Jeremy French

unread,
Jul 2, 2023, 5:41:39 PM7/2/23
to golang-nuts
Scrolling in code is bad - a necessary evil, obviously, but evil nonetheless.  Vertical scrolling is bad because it causes what we were looking at to move and our eyes have to track and/or reacquire what we were looking at.  It's obviously possible, but on the micro-scale it's expensive when you have to do it 1000 times per day of coding.  Horizontal scrolling is even worse because not only is it harder to track visually, but we don't have nearly the same ease of use on our mouse to horizontally scroll as we do to vertically scroll.   Just reacquiring where you were in the code takes up a miniscule amount of time and brain power that could be used for better purposes.  Again, it doesn't seem like much, and you may dismiss me as being melodramatic, but this discussion is a recurring one for a reason.  It's enough of a problem to bug people and want it to be different. 

So to speak to Dan's point, trading vertical scrolling for horizontal scrolling would be a move in the wrong direction.  But if you can reduce three lines to one WITHOUT causing horizontal scrolling, that benefits everyone, or at least everyone who uses a scroll wheel.

Harri L

unread,
Jul 2, 2023, 6:04:29 PM7/2/23
to golang-nuts
The sample block below is what we can have without any language updates. I will let you decide your thoughts about its readability, but please note that even the error strings are built automaticallyerror propagation with error traces, of course.  Deferred error handling seems to clarify certain things, like errors from a rollback in this case. The full playground. (Please remember to try the automatic error traces when playing with it.)

func (db *Database) MoneyTransfer(from, to *Account, amount int) (err error) {
defer err2.Handle(&err)

tx := try.To1(db.BeginTransaction())
defer err2.Handle(&err, func() {
if errRoll := tx.Rollback(); errRoll != nil {
err = fmt.Errorf("%w: ROLLBACK ERROR: %w", err, errRoll)
}
})

try.To(from.RecerveBalance(tx, amount))

defer err2.Handle(&err, func() { // optional, following sample's wording
err = fmt.Errorf("cannot %w", err)
})

try.To(from.Withdraw(tx, amount))
try.To(to.Deposit(tx, amount))
try.To(tx.Commit())

return nil
}


We have used the err2 package in production for business-critical systems for four years. And yes, I'm the author of the OSS package.

Dan Kortschak

unread,
Jul 2, 2023, 9:20:11 PM7/2/23
to golan...@googlegroups.com
On Sun, 2023-07-02 at 10:41 -0700, Jeremy French wrote:
> Scrolling in code is bad - a necessary evil, obviously, but evil
> nonetheless.  Vertical scrolling is bad because it causes what we
> were looking at to move and our eyes have to track and/or reacquire
> what we were looking at.  It's obviously possible, but on the micro-
> scale it's expensive when you have to do it 1000 times per day of
> coding.  Horizontal scrolling is even worse because not only is it
> harder to track visually, but we don't have nearly the same ease of
> use on our mouse to horizontally scroll as we do to vertically
> scroll.   Just reacquiring where you were in the code takes up a
> miniscule amount of time and brain power that could be used for
> better purposes.  Again, it doesn't seem like much, and you may
> dismiss me as being melodramatic, but this discussion is a recurring
> one for a reason.  It's enough of a problem to bug people and want it
> to be different. 
>
> So to speak to Dan's point, trading vertical scrolling for horizontal
> scrolling would be a move in the wrong direction.  But if you can
> reduce three lines to one WITHOUT causing horizontal scrolling, that
> benefits everyone, or at least everyone who uses a scroll wheel.

Scrolling is part of it, eye tracking is a less-obvious but more
significant issue; there is a reason newspaper columns are narrow — it
eases reading. Another part is burying semantics in a line, for
example, I find the common practice of `if err := f(); err != nil {...`
to be significantly less readable than the equivalent where the f call
is outside the if, and reject code in review with this unless it's
needed for scoping reasons. I sort of wish that this feature did not
exist, though — even though the exact same semantics are possible in
the language without it with the use of block scopes — I can see why it
exists.

In general I agree, with what you're saying though as a problem
statement. The problem is tuning the gofmt heuristics (if a single-line
if handling were adopted). This has been discussed in numerous issues
and rejected (for a variety of reasons), so it's unlikely to be
changed.

Mike Schinkel

unread,
Jul 3, 2023, 2:42:02 AM7/3/23
to golang-nuts
On Wednesday, June 28, 2023 at 1:30:41 PM UTC-4 Sven Anderson wrote:
I think for what you want to do you don't need any language extension. 

On Sunday, July 2, 2023 at 2:04:29 PM UTC-4 Harri L wrote:
The sample block below is what we can have without any language updates.

Many times when people ask for new language features it is possible to simulate what is being requested via some combination of convention and/or reusable package(s).

But sadly, at least in my experience, most teams are only motivated to continue the approach they chose initially and not interested in changing to a new non-standard approach, and this is the case for almost anything proposed as "something we can do today" unless it has already established itself as a defacto-standard way to approach the problem.

Although some conventions and some packages come close to achieving defacto-standard status — Cobra for CLI is the closest one I can think of for Go even though it is far from defacto — the use of most conventions and/or packages required a motivated team or at least a motivated individual. 

The simple fact is even if the core Go team adds a new feature for error handling, many will still not embrace it, at least not for a while.  But if there is any chance a motivated individual has to get an unmotivated team to adopt a new approach, it almost has to be an approach advocated for by the core Go team, and with new features that make that approach possible.

So while it may be great for motivated individuals and the rarer motivated teams to hear about how we can do things today without the new language features we are requesting — and the developer of the package that enables it is certainly motivated — there is little chance a "can do today" approach will address the reason people ask for a new language feature.  And especially for error handling improvements, which is near the top of the things people want to see improved in Go, per the Q1 2023 Go Developer Survey[1]. 

#fwiw

-Mike

 [1] Go Developer Survey 2023 Q1 Results - The Go Programming Language (golang.org)

Henry

unread,
Jul 3, 2023, 6:03:00 AM7/3/23
to golang-nuts
I don't think it has anything to do with motivation, Mike. The problem I see is that there is no good enough solution that offers a substantial benefit over the current approach. Many error handling proposals involve rearranging the code with a few line savings, and sometimes at the cost of readability. Note that there is no perfect solution to error handling to date. Even the conventional try-catch-finally has its own problems. I appreciate the Go Team's restraint in this matter.

Mike Schinkel

unread,
Jul 3, 2023, 10:40:23 PM7/3/23
to golang-nuts
Hi Henry,

For clarity, my comments were not intended to call out any lack of motivation — which you keyed on — but instead focus on the fact no silver bullet is likely to exist that addresses error handling without new language features, and that many Go developers want error handling addressed.

In hindsight I would remove the mention of motivation if I could so as not to be misinterpreted. #justfyi  #fwiw

-Mike

Shulhan

unread,
Jul 9, 2023, 10:43:58 AM7/9/23
to Mike Schinkel, golang-nuts
Hi Mike,

On Thu, 29 Jun 2023 22:04:01 -0700 (PDT)
Mike Schinkel <mi...@newclarity.net> wrote:

> There are many aspects of this proposal that I really like, and a few
> that I think need to be reconsidered.
>
> In general I like that it:
>
> 1. Does not try to handle errors *above *where the errors
> occurred, which is the pattern when using both *defer* and
> *recover(). *I personally find having to jump back much harder to
> reason about than jumping forward to find the error handler.
> 2. Does not require handling the error in a closure or other
> *func* as the former adds a lot of visual noise and the latter
> complicates by creating a different variable scope.
>
>
> It is in many ways similar to a proposal I have been planning on
> preparing but have no found the right opportunity. Possibly this is
> the right opportunity and maybe taking aspects of this and aspects of
> mine we could find a solution beneficial enough that Ian Lance Taylor
> would consider it a big enough improvement. I'll try to follow up on
> that soon.
>
> As for the specifics of this proposal:
>
> 1. I think *when err handle <label>* is a bit too much magic. I
> think it would be more Go-like if it were instead:
>
> *when err!=nil handle <label>*
>
> I know this makes for more repeated syntax, but within the scope of
> this proposal I do not think you can avoid it without it adding
> magic. I do think that could be addressed, and what I have in mind
> addresses that. But I wanted to first comment on your proposal, not
> introduce mine.

If we use WHEN like this, then there is no different with IF
statement.
The purpose of WHEN statement is to check for non-zero value.
Using IF statement, it could be written like these

if !ZeroValue(err) handle <label>

>
> 2. As for *check* as you have proposed it — handling zero return
> values — it seems to add more magic, and that it would only work in
> selected use-cases so it might be best to omit this from the proposal?

I am not sure on this one. The "check" keywords is proposed by parent
proposal [1], not by this proposal.

[1]
https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md

>
> 3. I do like the concept of labels that do not fall through to the
> code in the next label. However, it is not exactly clear how they
> should work. Do your handlers always have to have a *return*
> themselves?
>
>
> *func foo()error {*
> * ...*
> * when err != nil handle cleanup*
> * ...*
> * return nil*
>
> :*cleanup:*
> *doCleanup()*
> * return err*
> *}*

No. The body of handler is similar with body of function.
If the function does not expect return value than the execution of
function body will stop before the first handler defined.

Case example,

func main() {
hex, err := ioutil.ReadAll(os.Stdin)
when err handle fatal

data, err := parseHexdump(string(hex))
when err handle fatal

os.Stdout.Write(data)
// The function body stop here, the log.Fatal below will not be
// executed.
:fatal:
log.Fatal(err)
}


>
> 4. When you are in a handler, is there any way to correct the problem
> and *resume* or *retry* the code where you left off without resorting
> to using *goto* and labels?

No, but one can use by using recursive call.
IMO, the part of code that try handling error by resuming or retry
is an indication that it should be moved into separate function.

>
> 5. I do not think you need the new keyword *handle*, i.e. I think
> this would be sufficient to use *goto* instead since the syntax of
> your handler label with the preceding colon should be sufficient to
> distinguish between the two:
>
>
> *when err!=nil goto <label>*

That is possible, yes, I think it will minimise number of new syntax
introduced by this proposal.
I believe the implementation in the Go internal would be rather complex.

> 6. If you use *goto* instead of *handle* then you actually have two
> orthogonal features, the latter one being labels that do not fall
> through that can be used without when. Those could be proposed as a
> feature in their own right.
>
> 7. How would your proposal handle shared cleanup? As we can see from
> the following example I have to call *db.Rollback()* for every error
> case, and I would really rather only have to call it in one place in
> the case. Does your proposal have a solution for this that I missed?
>
>
> *func (db *Database) Transfer(from, to Account, amount int) (err
> error) {*
> *err = db.Begin()* *when err != nil **goto noTrans*
> *if **from*
> *.Balance() >= amount {*
> * goto noFunds* *}*
> *err = **from*
> *.Withdraw(amount)* *when err != nil **goto **noWithdraw*
>
> *err = to.Deposit(amount)* *when err != nil **goto **no**Deposit*
>
> *err = db.Commit()* *when err != nil **goto **no**Commit*
>
> *return nil*
> *:noCommit:*
> *db.Rollback()*
> *return fmt.Errorf("cannot commit; %w", err)*
> *:noDeposit:*
> *db.Rollback()*
> *return fmt.Errorf("cannot deposit; %w", err)*
> *:noWithdraw:*
> *db.Rollback()*
> *return fmt.Errorf("cannot withdraw; %w", err)*
> *:noTrans:*
> *db.Rollback()*
> *return fmt.Errorf("cannot begin transaction; %w", err)*
> *:noFunds:*
> *db.Rollback()* *return errors.New("no funds")*
> *}

Assume that we use the current proposal syntax, it could be written like
below (I am not fans of multiple joined "if ;" statements)

func (db *Database) Transfer(from, to Account, amount int) (err error) {
var tx *sql.Tx

tx, err = db.Begin()
when err handle errDbBegin

noBalance = from.Balance() < amount
when noBalance handle noFunds

err = from.Withdraw(amount)
when err handle errWithdraw

err = to.Deposit(amount)
when err handle errDeposit

err = tx.Commit()
when err handle errTxCommit

return nil

:errDbBegin:
err = fmt.Errorf("cannot begin transaction: %w", err)
return err
:errWithdraw:
err = fmt.Errorf("cannot withdraw: %w", err)
handle failed
:errDeposit:
err = fmt.Errorf("cannot deposit: %w", err)
handle failed
:errTxCommit:
err = fmt.Errorf("cannot commit: %w", err)
handle failed
:failed:
tx.Rollback()
return err
}

> *8. Using the example above, is there not a way to also
> annotate the error in a shared manner vs. having to have all the
> different handle labels and duplicated code?

If by "annotate the error" you mean adding log prefix before returning
the error, then yes.
Reply all
Reply to author
Forward
0 new messages