Error handling

1,147 views
Skip to first unread message

Michael MacInnis

unread,
Feb 13, 2021, 8:44:47 PM2/13/21
to golang-nuts
I've been playing around with reducing error handling boilerplate using standard language constructs.

I'm currently doing something that looks like this:

    import (
    )
    
    func do(name string) (err error) {
        check, handle := handle.Errorf(&err, "do(%s)", name); defer handle()
    
        s, err := works(name); check(err)
        
        // ...
    }

Other than the named return value and check being a hidden return, are there reasons I would want to avoid doing this? I assume others have tried similar things but I haven't stumbled across any similar packages.

Before using it in production I would probably want a linter that checks to make sure that the statement after handle.Error or handle.Errorf is defer blah, where blah is the name given to the second value returned by those functions and that all of this happens at the start of a function.

Michael.

Wojciech S. Czarnecki

unread,
Feb 14, 2021, 11:14:11 AM2/14/21
to golan...@googlegroups.com
Dnia 2021-02-13, o godz. 17:44:47
Michael MacInnis <michael.p...@gmail.com> napisał(a):

> I've been playing around with reducing error handling boilerplate

You're not alone. Hundreds of us went into such thinking in the first weeks
of reading/using Go - yet before we noticed how much more productive we
are with Go's "boilerplate" than we were in languages where handling errors
(failures) was "a problem of others", including future-us as "others".

Perceived consensus of the Go community is that "error handling boilerplate"
is a strong feature. I.e. in normal production software you MUST handle failures
and you should do it as close as possible to the source of said failure.

Go helps with that. Even team's proposal was finally retracted:
https://github.com/golang/go/issues/32437 Discussion there is lengthy, but worth
reading to sense why wider community considers "boilerplate" as asset.

Error handling proposals umbrella: https://github.com/golang/go/issues/40432

> Michael.

Hope this helps,

--
Wojciech S. Czarnecki
<< ^oo^ >> OHIR-RIPE

robert engels

unread,
Feb 14, 2021, 11:41:18 AM2/14/21
to Wojciech S. Czarnecki, golan...@googlegroups.com
I think ’strong census’ is not accurate - thus the discussions around improving error handling in Go2.

Many have commented here and elsewhere that the number one reason they don’t use Go is due to lack of exception handling.
> --
> 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/20210214171250.4377c454%40xmint.

Michael MacInnis

unread,
Feb 15, 2021, 11:19:46 AM2/15/21
to golang-nuts
Go helps with that. Even team's proposal was finally retracted:
https://github.com/golang/go/issues/32437 Discussion there is lengthy, but worth
reading to sense why wider community considers "boilerplate" as asset.

Thanks, I did follow the try proposal and the earlier check/handle proposal.

I agree that handling errors, and doing so close to where they occur, is a good thing. I also agree with the goals as outlined in the error handling problem statement. Specifically, that it would be nice if it was possible to make "error checks more lightweight, reducing the amount of Go program text dedicated to error checking". But that in doing so "error checks and error handling must remain explicit".

What I find interesting is how close we can get to something resembling try or check/handle with existing constructs.

While

f, err := os.Open(filename); check(err)
is not as terse as

f := try(os.Open(filename))

there is less much less text dedicated to error checking than with

f, err := os.Open(filename)
if err != nil {
// Handle err.
}

and passing err explicitly also means there isn't something to unwrap when debugging (to get to err) which I thought was an interesting objection to try.

Similarly, while

check, handle := handle.Errorf(&err, "copy %s %s", src, dst)
defer handle()

requires the enclosing function to use named return values and is not as terse as

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

it is close to the suggestion for decorating errors in the try proposal

defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

The problem is that forgetting to defer handle (in my version) means that check will cause an unrecovered panic. I'm also wondering if there are other interactions that I've overlooked. It is these more mechanism than policy considerations where I'm looking for feedback.

Michael.

Volker Dobler

unread,
Feb 15, 2021, 12:25:33 PM2/15/21
to golang-nuts
I think there is strong consensus, that the current style of error handling is currently the best option. Nobody has been able to come up with something better (really better, not just more comfortable while ignoring hefty drawbacks).

It is true that a loud minority seems to miss exceptions to a point where they are unwilling to even try the language. How much their expertise counts if they never really worked with proper error handling in Go is debatable. Having worked with error returns and exceptions I can tell you that proper error handling with exceptions is much more painful than with errors. But of course: If your infrastructure, your business requirements and your acceptance criteria allow for making any problem a problem of someone else than exceptions are godsend.

V.

Arnaud Delobelle

unread,
Feb 15, 2021, 1:18:32 PM2/15/21
to Michael MacInnis, golang-nuts
I do sometimes do something similar, but without the check() function.
The exit points are explicit, it is guaranteed that errors will be
wrapped.

func do(name string) (err error) {
defer PWrapf(&err, "do(%s)", name)

s, err := works(name);
if err != nil {
return err
}

// ...
}

// PWrapf applies Wrapf to an error pointer
func PWrapf(err *error, format string, args ...interface{}) {
*err = errors.Wrapf(*err, format, args...)
> --
> 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/cbe53d07-4f99-49fa-a708-dcb85b1aff5bn%40googlegroups.com.

robert engels

unread,
Feb 15, 2021, 1:53:43 PM2/15/21
to Volker Dobler, golang-nuts
And I will tell you that after working with error returns and exceptions, exceptions are superior - but much depends on the complexity of the system. For small system level tools that are pieced together via pipes, C error handling is fine - because the process serves as the exception handling point. For complex server long-lived processes, exception handling makes it much easier to design and deliver secure fault tolerant systems. Resilient Unix relies on the ability to start new processes to handle requests, track DoS attacks, faulty clients, etc. Process termination is a part of this. The Go runtime does not fit this model, so it needs to replicate it.

People write bad exception code - people write bad error return code. It’s easier to write and maintain good exception code. To this day, very few major systems are written in Go - at some point we have to ask honestly why - I think Go’s  error handling is an important factor here.

Go can probably get there without exceptions, but it entails standardized error declarations, wrapping, inspection, logging, etc. I think the Go2 error handling proposals attempt this.

David Skinner

unread,
Feb 16, 2021, 1:46:07 PM2/16/21
to golang-nuts
I wanted to express my thanks to Mickael McKinnus for his research.

I am someone who is quite happy with the error handling in Go as it lets me implement whatever I like, I must say it is obviously flawed from the standpoint that the proper constructs are not part of the language but part of our training. The discreet patterns I use are:
  • Handle errors that can be handled close to where they occur.
  • Report errors that cannot be handled because they were caused by the calling function passing a bad parameter.
  • Panic on errors that prevent a function that MustComplete from completing because they cannot be handled. 
  • Try a MustComplete() with recover from the panic that reports to the User and or Developer the nature of the problem and how to resolve it manually or at least add it to issues.
  • Ensure that no matter what happens, you do not corrupt the HDD, lockup the hardware, CTD, or leave unexpected artefacts on the screen.
There is no one best way of dealing with errors but there are patterns for dealing with errors, if you only learn one pattern then you will not be writing good code (if you only have a hammer, everything looks like a nail).

Golang does not have error patterns that are clear and distinct for novice programmers and it does not inhibit advanced programmers from dealing with errors in whatever way they think appropriate.

A useful abstraction would be to simplify the error handling by identification of the type of pattern desired and use the most concise and yet easily understood way of writing it. Go has tools in place to allow each user to create a preprocessor for error handling or any other purpose but it would be nice if we were all on the same page.

Henry

unread,
Feb 17, 2021, 10:42:01 PM2/17/21
to golang-nuts
I actually prefer the explicit error handling:
```
f, err := os.Open("path")
if err != nil {
   //handle error
}
```  
When you are reading the code to look whether a certain condition has been handled, the explicit branching statement makes it easier to scan. 

robert engels

unread,
Feb 17, 2021, 11:01:47 PM2/17/21
to Henry, golang-nuts
But - the issue is that most of the time - for complex systems - the errors cannot be handled where they are encountered - even in Go, most of the error handling becomes boilerplate returning the error detected and expecting some higher level to handle it.

With exceptions this is enforced, or at least codified, and with GC/RAI the system with try/finally can have a coherent view of the current state. You can do this with Go, but it is not “easy” by any means. Exceptions force the error handling up the stack until the layer is capable of handling it.

Go - whether people like this or not - is VERY close to Java (because of the “runtime”) - and both suffer from “runtime overhead/startup cost” - so Go processes become “servers”. Writing resilient user space servers is difficult in Java and more so in Go due to the lack of exceptions.

Beyond error handling, Go (and Java) are weak in resource management - which opens another door of bugs/problems related to “user space OS”.

Every OS has exceptions - i.e. process failure due to unforeseen reasons.

There are other tactical problems that exceptions solve that error returns do not. Think of a complex multi-component system - and a module decides to expose a new “class of errors” - with exceptions you can force existing well-written code to break at compile time (unknown exception in the hierarchy), with error returns this is not possible (at least not in Go with non-standardized error “classes”). Any there are many similar problems.

These are all issues C programmers struggled with for years - and why error handling was changed in C++.


Miguel Angel Rivera Notararigo

unread,
Feb 18, 2021, 3:30:26 AM2/18/21
to robert engels, Henry, golang-nuts
What would be the difference between using exceptions/deferring a generalized error wrapping, and panic-recover?

Amnon

unread,
Feb 18, 2021, 3:34:35 AM2/18/21
to golang-nuts
OT, but what has happened to the Go 2 error handling proposals?
I heard that the original check proposal was abandoned because of the 
unfortunate interaction with defer. 

Any updates?

Brian Candler

unread,
Feb 18, 2021, 5:13:32 AM2/18/21
to golang-nuts
On Thursday, 18 February 2021 at 04:01:47 UTC ren...@ix.netcom.com wrote:
Go - whether people like this or not - is VERY close to Java (because of the “runtime”) - and both suffer from “runtime overhead/startup cost”

I have used one-shot Java CLI tools that take literally 4 or 5 seconds to start up (Kafka CLI for example); Go tools are more like 5ms in my experience, thanks to being compiled ahead-of-time into native code.  So I don't think that's a fair comparison.

I think we may be discussing a different use case here.  In the Unix world, if you want something to be resilient against a total process crash, then you spawn it as a child, you supervise it, and restart it if necessary.  What you get back is not so much an exception, as an exit code.

Among programming languages, the one I can think of with a similar concept is Erlang, which provides for a process supervision tree.  Erlang processes are the equivalent of goroutines.  A process terminates with an exit reason.  You can link related processes together.  Patterns in go which provide for similar supervision trees of goroutines would be an interesting area of study.  (Note that in Erlang you tend to use processes for more than just concurrency. There is no variable rebinding, so you need a function to call itself recursively to model mutable state: so you stick that function in a process and send messages to it)

However, I think the types of errors being discussed in this thread are just part of sequential code operation.  For example, when you open a file:
* it may be successful, you get back a file handle
* there may be a "normal" failure because of user-supplied data, e.g. "a file with that name does not exist"
* there may be a "system" failure, e.g. "I/O error on disk"

When you read from the open file, similarly there are other errors: e.g. "I expected that file to contain valid JSON, but it doesn't".  These cases need to be distinguished and handled somewhere.

Kevin Chadwick

unread,
Feb 18, 2021, 5:18:05 AM2/18/21
to golang-nuts
On 2/18/21 4:01 AM, robert engels wrote:
> But - the issue is that most of the time - for complex systems - the errors
> cannot be handled where they are encountered - even in Go, most of the error
> handling becomes boilerplate returning the error detected and expecting some
> higher level to handle it.

Occasionally I think what could I add here at the points that you call
boilerplate. However, I almost always add context at every level. The benefit is
that I hardly ever have to pull out a debugger or even read the code aside from
the failure point or stdlib code. The debugger is useful for more subtle issues,
but a big, time waste, most of the time.

You can also create test errors easily by wrapping with the recent changes.
Wrapped errors act, in a way like exceptions. In fact, I log the latest
test/wrapped error and any previous one in the chain. So that I may see any one
provided by the stdlib as well as my own.

What you are proposing doesn't match up with Gos ease of picking up and running
with the language with good outcomes because exceptions are less likely to be
handled than errors by most devs and also in most situations. It also doesn't
marry up with the policy of trying to keep one way of doing things, for similar
reasons. Isn't there a package, that you could use?

Carla Pfaff

unread,
Feb 18, 2021, 6:13:32 AM2/18/21
to golang-nuts
On Thursday, 18 February 2021 at 09:34:35 UTC+1 Amnon wrote:
OT, but what has happened to the Go 2 error handling proposals?
I heard that the original check proposal was abandoned because of the 
unfortunate interaction with defer. 

Any updates?

After the "check" proposal the Go team made another proposal, the "try" proposal, but it was rejected as well. Now error handling is on the back-burner and they are focusing on generics:
"Also, realistically, since we don't have unlimited resources, I see thinking about language support for error handling go on the back-burner for a bit in favor of more progress on other fronts, most notably work on generics"

Robert Engels

unread,
Feb 18, 2021, 9:53:46 AM2/18/21
to Carla Pfaff, golang-nuts
To clarify, the Java like/runtime reference I made does not strictly refer to startup costs - I am also referring to runtime costs as in additional threads for GC, housekeeping, overall binary size, etc. I don’t think ‘single shot, short lived processes’ are the typical Go paradigm - they are usually larger , multi layer, long lived “server” processes. It’s my opinion that Gos error handling is a problem for these types. I am not saying it can’t be done but it’s harder to design/deliver/maintain.

Exceptions (used properly) provide a lot of information when reading the code.  I don’t get the same feedback with Gos error returns. 

On Feb 18, 2021, at 5:13 AM, 'Carla Pfaff' via golang-nuts <golan...@googlegroups.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.

Kevin Chadwick

unread,
Feb 18, 2021, 11:39:38 AM2/18/21
to golan...@googlegroups.com

>don’t think ‘single shot, short lived processes’ are the typical Go
>paradigm - they are usually larger , multi layer, long lived “server”
>processes. It’s my opinion that Gos error handling is a problem for
>these types. I am not saying it can’t be done but it’s harder to
>design/deliver/maintain.
>

I can't agree. Long lived go server processes are made of many single shot worker tasks that can send errors back just the same.

>Exceptions (used properly) provide a lot of information when reading
>the code. I don’t get the same feedback with Gos error returns.

AFAICT, this is just structuring. There is nothing stopping your server processes from having a documented error processor, handling returned error types, possibly even from distributed services.

The only real difference is if the errors are tunnelled in plain sight or behind the scenes. You could store rather than returning errors if you wanted to. I don't think Go users should be encouraged to hide errors away.

Robert Engels

unread,
Feb 18, 2021, 2:27:33 PM2/18/21
to Kevin Chadwick, golan...@googlegroups.com
Yes but without robust error information (stack trace, typed error) it is very hard to write that top-level handler - at least not a robust one. Plus you are relying on the proper use of defer etc up the chain. This is much simpler with exceptions - to me anyway.

> On Feb 18, 2021, at 10:39 AM, Kevin Chadwick <m8il...@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/65F3B450-5507-4E3C-A09B-905DCF4C0006%40gmail.com.

Michael MacInnis

unread,
Feb 18, 2021, 7:01:15 PM2/18/21
to golang-nuts
At the risk of getting caught in the crossfire, I will point out again that I just found it interesting how close it was possible to get to something like check/handle with just standard language constructs. If you'll excuse the cuddled checks, I think this:

    func CopyFile(src, dst string) (err error) {
        check, done := handle.Errorf(&err, "copy %s %s", src, dst);
        defer done()

        r, err := os.Open(src); check(err)
        defer r.Close()

        w, err := os.Create(dst); check(err)
        defer handle.Chain(&err, func() {
            w.Close()
            os.Remove(dst)
        })

        _, err = io.Copy(w, r); check(err)
        return w.Close()
    }

looks pretty similar to this:

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

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

        w := check os.Create(dst)
        handle err {
            w.Close()
            os.Remove(dst) // (only if a check fails)
        }

        check io.Copy(w, r)
        check w.Close()
        return nil
        }

I'll probably continue playing with this. If others would like to take a look the code is here:

    https://github.com/michaelmacinnis/handle

I'm particularly interested in any problems I may have overlooked and similar packages that may exist but that I haven't stumbled across.

Thanks,

Michael.

robert engels

unread,
Feb 18, 2021, 8:28:55 PM2/18/21
to Michael MacInnis, golang-nuts
There's no crossfire :)

I think most of the issues I have can probably be addressed with some standardized packages without converting panic/recover into full-blown exceptions and making them the default. The key is “standardized”, which is why I’m sad to see lack of progress towards a new paradigm for Go2.

L Godioleskky

unread,
Feb 20, 2021, 11:31:44 AM2/20/21
to golang-nuts
Rust lang, very early in its evolution, saw the need to create its operator '?'  to more efficiently manage error handling. But the guardians of Go lang have resisted any changes to its clumsy method of error handling despite it being a major concern of Go users for a very long time. 

roger peppe

unread,
Feb 20, 2021, 1:31:31 PM2/20/21
to L Godioleskky, golang-nuts


On Sat, 20 Feb 2021, 16:31 L Godioleskky, <lgo...@gmail.com> wrote:
Rust lang, very early in its evolution, saw the need to create its operator '?'  to more efficiently manage error handling. But the guardians of Go lang have resisted any changes to its clumsy method of error handling despite it being a major concern of Go users for a very long time. 

Actually the "guardians of Go" (by which I guess you mean the Go team at Google) tried quite hard recently to propose an improved way of handling errors, but it was resisted by "Go users". So what you're saying is just not true, I'm afraid.



On Sunday, February 14, 2021 at 11:14:11 AM UTC-5 ohir wrote:
Dnia 2021-02-13, o godz. 17:44:47
Michael MacInnis <michael.p...@gmail.com> napisał(a):

> I've been playing around with reducing error handling boilerplate

You're not alone. Hundreds of us went into such thinking in the first weeks
of reading/using Go - yet before we noticed how much more productive we
are with Go's "boilerplate" than we were in languages where handling errors
(failures) was "a problem of others", including future-us as "others".

Perceived consensus of the Go community is that "error handling boilerplate"
is a strong feature. I.e. in normal production software you MUST handle failures
and you should do it as close as possible to the source of said failure.

Go helps with that. Even team's proposal was finally retracted:
https://github.com/golang/go/issues/32437 Discussion there is lengthy, but worth
reading to sense why wider community considers "boilerplate" as asset.

Error handling proposals umbrella: https://github.com/golang/go/issues/40432

> Michael.

Hope this helps,

--
Wojciech S. Czarnecki
<< ^oo^ >> OHIR-RIPE

--
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.

Matthew Holiday

unread,
Feb 20, 2021, 2:39:45 PM2/20/21
to roger peppe, L Godioleskky, golang-nuts
Roger beat me to it.

But allow me to rephrase,

"The users of Go for a long time have resisted any changes to its simple, clear method of error handling despite it being a major concern of folks who don't use Go much." *

* I'm referring to the original survey, which was worded along the lines of "what keeps you from adopting Go?"
(implying that the responders are those who haven't adopted Go)

Any type of error handling that creates invisible returns in a function is a bad idea IMNSHO (an opinion backed up by various researches into the complexity of exception handling). Speaking for myself, I'd like to retain that quality of Go whereby "the code does exactly what it says on the page."




--
Matt Holiday
Senior Gopher, Marketing Technologies

620 Eighth Avenue

New York, NY 10018

matthew...@nytimes.com

Robert Engels

unread,
Feb 20, 2021, 3:11:38 PM2/20/21
to Matthew Holiday, roger peppe, L Godioleskky, golang-nuts
Can you clarify what you mean mean by “the code does exactly what it shows on the page”? How do you know by looking at the code, or even compiling the code, that all possible errors returned by a function are handled? That to me is biggest difficult in reading (or using) others Go code. Exceptions (well written) handle this by declaring all possible error (or categories) thrown by the method. 

This seems a real problem with long term maintenance of Go code. 

On Feb 20, 2021, at 1:39 PM, Matthew Holiday <matthew...@nytimes.com> wrote:



Ian Lance Taylor

unread,
Feb 20, 2021, 3:30:54 PM2/20/21
to Robert Engels, Matthew Holiday, roger peppe, L Godioleskky, golang-nuts
On Sat, Feb 20, 2021 at 12:11 PM Robert Engels <ren...@ix.netcom.com> wrote:
>
> Can you clarify what you mean mean by “the code does exactly what it shows on the page”? How do you know by looking at the code, or even compiling the code, that all possible errors returned by a function are handled? That to me is biggest difficult in reading (or using) others Go code. Exceptions (well written) handle this by declaring all possible error (or categories) thrown by the method.
>
> This seems a real problem with long term maintenance of Go code.

When you say that exceptions means declaring all possible errors
thrown by the method, it seems to me that you are talking about
checked exceptions. There is pretty good evidence that real programs
in practice do not list all possible error categories, even though
ideally they should. Instead, in practice, they fall back to
unchecked exceptions.

So I think that your contrast to Go is an ideal that experience has
shown is very difficult to achieve. It's not an approach that leads
to better long term maintenance in practice, even if it should in an
ideal world.

(Incidentally, to answer the question in your first sentence directly,
Go code does indeed do exactly what it shows on the page. You are
challenging that not by saying that it does something else, but by
saying that that is not enough.)

Ian

Matthew Holiday

unread,
Feb 20, 2021, 3:31:22 PM2/20/21
to Robert Engels, roger peppe, L Godioleskky, golang-nuts
I'm referring to errors found in the function (i.e., by calling other functions). It's the responsibility of the callers of a function to handle the errors it returns, and not the function itself. How can one function claim responsibility for the error handling strategy of all programs using it?

Yes, I suppose in some sense exceptions guarantee all errors are handled somewhere. Unfortunately, what I've seen of exception handling over the years is that it's often "log the stack trace and keep moving", which isn't all that useful, and tends to cover up real bugs. A better approach would be not to catch exceptions at all, and let them crash the program (which is what Go's panic will do); this ensures the bugs are really handled by removing them from the program.

[And given this type of exception "handling" it's not much value to the author of a single function to know that the errors will all be "handled" somewhere.]

Unfortunately, exception handling languages tend to put all types of errors, normal and abnormal *, into the same basket. Which means you can't do sensible error handling for, e.g., JSON that doesn't decode, while allowing the program to crash when there's a logic bug. (For non-safety critical software, a crash failure is typically the safest way to fail. Safety-critical software, on the other hand, avoids exception handling like the plague.)

* A normal error is some behavior that can reasonably be expected, such as "file not found" or "no route to host", etc. Abnormal errors are logic bugs in the program.

An aside:

Assuming you had a cyclomatic complexity calculator that took exceptions into consideration, such that exceptions passing through a function counted as a branch, what kind of numbers would you get? Probably pretty awful, given just about any line of code would be capable of throwing an exception. But exceptions typically aren't counted, so that functions are thought to be far less complex than they really are in the presence of exceptions.

Invisible (magic) return paths through a function go against the notion of "the code does what it says on the page".

Michael Ellis

unread,
Feb 20, 2021, 4:21:10 PM2/20/21
to golang-nuts
FWIW,  I've put together a tiny package that, with some tradeoffs, seems useful for reducing boilerplate in the common case where a function simply wants to return an error to its caller.  


The code is almost trivial. It consists of two small functions, ro.RecoverOn( err *error) and ro.ReturnOn(err error), used as follows:


func myfunc() (err error) { 
    defer ro.RecoverOn(&err) 

    err = SomeFunctionCall() 
    ro.ReturnOn(err)

    // Do more stuff 
    // ... 
    return 
}

ReturnOn panics if err is not nil.

RecoverOn recovers from the panic raised by ReturnOn and the function exits with whatever error value would have been returned normally. RecoverOn does not interfere with panics arising outside of ReturnOn.

Benefits and tradeoffs (coding discipline, debugging, performance) are discussed in the README.

Feedback welcomed either in this thread or in the repo issues.

Robert Engels

unread,
Feb 20, 2021, 6:02:40 PM2/20/21
to Ian Lance Taylor, Matthew Holiday, roger peppe, L Godioleskky, golang-nuts
I consider checked vs unchecked exceptions “well written” or even valid code. Checked exceptions are similar to Gos error return except more/easier/better? compile and coding time validation. Unchecked exceptions are similar to panic recover except you have more information.

There is no difference between checked exceptions and Go’s error return in terms of responsibility - but exceptions are more easily enforced.

When it comes to unchecked exceptions a lot depends on the use case. For instance, the consider an illegal state exception. Clearly a programming error, but how it is handled at higher levels depends... maybe roll back the current transaction and report an error to the caller, maybe crash the server? It all depends on the context , risk, capabilities - easier for higher layer to make this call than a low level function. Coupled with RAI you can create very robust and reliable systems.

> On Feb 20, 2021, at 2:30 PM, Ian Lance Taylor <ia...@golang.org> wrote:

Robert Engels

unread,
Feb 20, 2021, 6:06:01 PM2/20/21
to Matthew Holiday, roger peppe, L Godioleskky, golang-nuts
Some developers build sheds others build skyscrapers. Exceptions let you build skyscrapers - but not everyone needs a skyscraper. 

I’ve use both methods extensively in my career - well written exception code is far easier to write and maintain for more complex systems.

On Feb 20, 2021, at 2:31 PM, Matthew Holiday <matthew...@nytimes.com> wrote:



Michael MacInnis

unread,
Feb 20, 2021, 6:28:43 PM2/20/21
to golang-nuts
Neat. What you and I are doing is very similar. Particularly how the handler is "bound" to the error and checks to make sure that the error is non-nil before attempting to recover.

The differences I see in our approaches are as follows:


I support wrapping an error or performing some arbitrary action. This wouldn't be difficult for you to add - just add a func() parameter to RecoverOn that is invoked when *err != nil. If you did that then we would both support performing actions whether an early return was triggered by check/ro.ReturnOn or a regular return with err set (assuming named return values).


I bind the check function (what you call ro.ReturnOn) to the error so that it can set that when passed a non-nil error. This allows check to be used directly, for example, on a function that only returns an error:

check(functionThatReturnsAnError())

Instead of having to do:

err = functionThatReturnsAnError()
ro.ReturnOn(err)

Binding the check and done functions means returning them both from a call to handle.Error. This adds one more line of code (defer done()) but means that check doesn't exist without this step and given that Go complains about unused variables it takes some work to forget to do something with done. My hope is that this significantly reduces the risk of an unhandled panic. (It's still possible to just call done and forget to defer it or name done _ so that the compiler won't complain, etc., etc.) As an added bonus people can call this pair of functions whatever they want (check/done, try/handle, ReturnOn/RecoverOn...). When I first posted I thought it would be neat to do:

check, handle := handle.Error // ...
so that the shadowing would make it impossible to call handle.Error again. But then I added Chain.

(It would be nice to get the handle.Errorf case down to one line but I haven't figured out a way to do that. Maybe something with how arguments to deferred function are evaluated but the function call itself isn't...)


In check I wrap the error in an unexported type:

type failure struct {
error
}

If this manages to slip by the idea is that it should be reported as an "unhandled error" plus the text of the actual error. It is the responsibility of done to unwrap the error and perform any actions on that. I'm paranoid about invoking recover, only invoke it when the error is the unexported failure type and even then check to make sure that the recovered value matches the error value. It is possible to always recover and then if we recover something we weren't supposed to, re-panic, but then we lose all the context for the original panic.


Looking back at the error handling problem outline I realized that I did not have the ability to add additional error handling actions. I added Chain a few days after my original post.


Michael.

Wojciech S. Czarnecki

unread,
Feb 20, 2021, 6:54:39 PM2/20/21
to golan...@googlegroups.com
Dnia 2021-02-20, o godz. 13:21:09
Michael Ellis <michael...@gmail.com> napisał(a):

> FWIW, I've put together a tiny package that, with some tradeoffs, seems
> useful for reducing boilerplate in the common case where a function simply
> wants to return an error to its caller.

> The code is almost trivial. It consists of two small functions,
> ro.RecoverOn( err *error) and ro.ReturnOn(err error), used as follows:

> in the common case where a function simply wants to return an error to its caller.

There is no trade off here for me (an likely many others). It is idiomatic versus weird:

1. hit !er<space> move on coding (in fact MY shortcut puts panic instead of return).

2. USE: import (write) a module, setup defer stack, confuse first-time reader, write a call, move on.

Note that both versions on my vim take just one line off the screen estate. This is true for any IDE able to fold. OK - idiomatic would take two lines more if code is viewed in pager or using MS Word.

> in the common case where a function simply wants to return an error to its caller.

It is NOT a "common" case.

In the "boring" production code we are expected that any service exposed to the end-user will never refuse to work, our code must try really hard to complete, retrying at other server here, then at other city's or continent server room until its real task can be successfully done. Resources (eg. network services) needs to be acquired, then vetted, then operated on, then finally released in a consistent state.

So we don't throw "something went wrong" (ie. unhandled exception) up. Nor we do "Return if any error". We do check errors, then
retry in loops. Go explicit, IN PLACE handling helps with that alot.

TC,

Michael MacInnis

unread,
Feb 20, 2021, 7:48:09 PM2/20/21
to golang-nuts
I don't believe anyone is talking about not handling errors. There are functions that perform a sequence of actions and need to go no further when an error is encountered. An example of this type of function is presented in the "Errors are Values" post:


Another example is the CopyFile function from "Error Handling - Problem Overview":


In these cases the caller may be better equipped to know what to do next when there is an error.

The "Errors are Values" post gives some examples of using the language to simplify error handling. The check/handle and try proposals do the same thing but by proposing language changes. I don't believe any of them are proposing not checking and/or not handling errors.

I can tell you most emphatically that I am not proposing not checking and/or not handling errors. I'd like to know if, for functions like the ones just mentioned, a pattern exists for "eliminating much of the boilerplate that arises if every error is checked with a rote if statement." And further if that pattern can be implemented as a library/package with acceptable trade offs.

Michael.

Wojciech S. Czarnecki

unread,
Feb 21, 2021, 11:11:00 AM2/21/21
to golan...@googlegroups.com
Dnia 2021-02-20, o godz. 16:48:09
Michael MacInnis <michael.p...@gmail.com> napisał(a):

> I can tell you most emphatically that I am not proposing not checking
> and/or not handling errors.

I did not stated that you won't. Cited piece was directed at "just return err"
code stressing that it is not that common in the production code, mine's.

> I'd like to know if, for functions like the
> ones just mentioned, a pattern exists for "eliminating

> much of the boilerplate that arises if every error is checked with a rote if

I said twice that for me it is not "boilerplate" but much appreciated feature
of the language whose creators knew and acknowledged that every piece of
code is written once by one developer but is then being read over and over
by way more people than one.

And I have had not less emphatically told that where there is a function
that just returns with err, even in "many places", the bytewise cost of return
is some 30 bytes usually placed by the IDE with three strokes of mine.

For these three strokes where I just return (now):

As a reader I gained clear information that here something was checked then
function just spilled error up *but* I have the surrounding context at sight.

As an author I gained - now or later:
- A ready space to comment on and/or decorate the error value.
- If I am decorating or creating error value, I am right in scope.
- If this is recoverable failure, I can try to recover it now.

> And further if that pattern can be implemented as a library/package
> with acceptable trade offs.

In this thread it was proven that this antipattern can be implemented
as a library/package - using import, then setup, then a call to obscure
function - all to replace a 26 or 30 bytes long idiom - by Michael Ellis.

> I'd like to know if, for functions like the
> If … a pattern exists for "eliminating [ these "ifs" ]

Yes it does exist. I once upon a time did a "fail" package and accompanying
generator to implement such "if-less" pattern to use in a cases where cost
of acquiring a resource is high and a bunch (say 8-10) of such resources is
needed to complete the task. It was a waste of time, as new people need to
be taught to read and understand it anyway. So it was abandoned due to
onboarding issues. It really was not worth the time.

``` go
for fail.With(err) { // bind an error type variable,
s, abort := fail.Simple() // set up a single pass loop
abort.Where(err != nil, !ok, z < 0) // set common "failure" condition(s)

z := z // test z < 0 (a "checked assignment")
r, ok := <-ch // test !ok
x, y, z, err := f(z, r) // test err != nil, then test z < 0
abort.If(x > y) // test x > y (a "plain test")
x, y, err = m(x, y) // test err != nil
return x, y, z, nil // can be prepended by an optional 'pass:'

abort:
fmt.Printf("‖ %s: \t( %s )\n", err.Error(), s.Trig)
return 0, 0, 0, err
}
// output:
‖ failed Test: ( x > y ) // for x > y
‖ failed Assignment: ( !ok ) // for closed ch
‖ failed Assignment: ( z < 0 ) // for z < 0
‖ Whatever err said: ( err != nil ) // for failed f(z, r) and m(x, y)
```

Early iteration (live example) to be seen here: https://play.golang.org/p/W35BVsZnZ-q

Hope this helps,

robert engels

unread,
Feb 21, 2021, 12:23:52 PM2/21/21
to Wojciech S. Czarnecki, golan...@googlegroups.com
Can someone please explain the benefit of ‘error return’ over ‘checked exceptions’ ? I have made the point a few times and it goes to crickets - I have to believe that is because there is none, or it is difficult to communicate.

The ‘handle where they occur claim’ is weak, as you can handle exceptions at the call site with similar LOC and constructs - but why when in most cases (of non-trivial software) the error value is just returned up. No offense to Wojciech - the rpc transport layer doesn’t do the retires, it passes up an error, and a higher level performs retry logic, if all retries fail, it will pass up another error that the rpc failed, etc. This pattern is used throughout well designed software. If you hard using this pattern you are creating very hard to maintain software.

Checked exceptions are ‘error values’ with more information (e.g stack traces, causes, etc.), and can be enforced by the compiler (declared) and inspected by modern IDE’s for code complete. ‘error returns’ have none of these features. The only valid claim is that they are more expensive than error values, BUT you should not be using exceptions - or error values - for flow control. This is the fundamental problem to me with Go’s error handling - the flow control and errors are mixed leading to very difficult code to read & validate that all potential paths are handled now and in the future.
> --
> 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/20210221171014.2c247c6d%40xmint.

Ian Lance Taylor

unread,
Feb 21, 2021, 4:18:19 PM2/21/21
to robert engels, Wojciech S. Czarnecki, golang-nuts
On Sun, Feb 21, 2021 at 9:23 AM robert engels <ren...@ix.netcom.com> wrote:
>
> Can someone please explain the benefit of ‘error return’ over ‘checked exceptions’ ? I have made the point a few times and it goes to crickets - I have to believe that is because there is none, or it is difficult to communicate.

As I wrote earlier, I believe that experience shows that it is
extremely difficult to write a large scale program with multiple
maintainers that uses checked exceptions. In practice, programs
always fall back to unchecked exceptions. So I would say that one
benefit of error return over checked exceptions is simply that error
returns scale to large programs and many maintainers.

Another benefit is described by Rob's blog post, which I'm sure you've
seen: Errors are values (https://blog.golang.org/errors-are-values).

More generally, errors are not exceptional conditions. They are part
and parcel of how programs work. They are part of the normal program
flow, and they should be part of that flow. They shouldn't be
regarded as exceptional and unusual flow.

I'm not going to claim that any of these benefits are slam dunks. If
there were obvious solutions to how to handle errors, nobody would
still be discussing different approaches. But I am claiming that Go's
approach to error handling has real benefits.


> The ‘handle where they occur claim’ is weak, as you can handle exceptions at the call site with similar LOC and constructs - but why when in most cases (of non-trivial software) the error value is just returned up. No offense to Wojciech - the rpc transport layer doesn’t do the retires, it passes up an error, and a higher level performs retry logic, if all retries fail, it will pass up another error that the rpc failed, etc. This pattern is used throughout well designed software. If you hard using this pattern you are creating very hard to maintain software.

I can't agree that in most cases the error is just returned up. I
would agree that within a single package an error is often (but not
always) returned up without change. But in large Go programs that
I've seen, when error values cross package boundaries, they are almost
always annotated with additional information.


> Checked exceptions are ‘error values’ with more information (e.g stack traces, causes, etc.), and can be enforced by the compiler (declared) and inspected by modern IDE’s for code complete. ‘error returns’ have none of these features. The only valid claim is that they are more expensive than error values, BUT you should not be using exceptions - or error values - for flow control. This is the fundamental problem to me with Go’s error handling - the flow control and errors are mixed leading to very difficult code to read & validate that all potential paths are handled now and in the future.

My opinion, which contradicts yours, is that when the flow control is
written down on the page, as it is in Go, the flow is easy to read,
and it is much easier to check that all potential paths are handled.
With exceptions the control flow is not written down, so it is easier
to miss cases where an exception can arise. With exceptions the
normal control flow is easier to see, but the error control flow is
harder. And since the error control flow happens less frequently,
it's more important to make it clearly visible to the original
programmer and, more importantly, to the future maintainer.

Again, no slam dunks here. Reasonable people can disagree. But I
think the Go position is entirely defensible for large scale programs
with many programmers.

Ian

robert engels

unread,
Feb 21, 2021, 6:01:51 PM2/21/21
to Ian Lance Taylor, Wojciech S. Czarnecki, golang-nuts
See below:

> On Feb 21, 2021, at 3:17 PM, Ian Lance Taylor <ia...@golang.org> wrote:
>
> On Sun, Feb 21, 2021 at 9:23 AM robert engels <ren...@ix.netcom.com> wrote:
>>
>> Can someone please explain the benefit of ‘error return’ over ‘checked exceptions’ ? I have made the point a few times and it goes to crickets - I have to believe that is because there is none, or it is difficult to communicate.
>
> As I wrote earlier, I believe that experience shows that it is
> extremely difficult to write a large scale program with multiple
> maintainers that uses checked exceptions. In practice, programs
> always fall back to unchecked exceptions. So I would say that one
> benefit of error return over checked exceptions is simply that error
> returns scale to large programs and many maintainers.
>
I don’t think answering, “people fall back to unchecked exceptions” is an answer to “how do error returns and checked exceptions differ”.

> Another benefit is described by Rob's blog post, which I'm sure you've
> seen: Errors are values (https://blog.golang.org/errors-are-values).
>
> More generally, errors are not exceptional conditions. They are part
> and parcel of how programs work. They are part of the normal program
> flow, and they should be part of that flow. They shouldn't be
> regarded as exceptional and unusual flow.
>
Yes, with all due respect to Rob, we'll probably disagree on that blog post. Take the Scan() example. This is a justification of how to write code that isn’t painful when you don't have exceptions - instead it puts the burden on the writer of Scan() to set a boolean internally to ‘don’t do anything’, write an additional method Err(), check the boolean on all other methods, etc., then the caller needs to remember to call Err() after all processing - and the code might loop through a billion elements doing nothing - potentially creating huge inefficiencies. Lastly, a reader of the code where the developer had failed to call Err() would not know unless they were intimately aware of the particular API - this is not good for long term maintenance.

I can provide examples for each case in the blog as to why it seems more 'defensive justification’ rather than evidence of being better.

> I'm not going to claim that any of these benefits are slam dunks. If
> there were obvious solutions to how to handle errors, nobody would
> still be discussing different approaches. But I am claiming that Go's
> approach to error handling has real benefits.
>
>
>> The ‘handle where they occur claim’ is weak, as you can handle exceptions at the call site with similar LOC and constructs - but why when in most cases (of non-trivial software) the error value is just returned up. No offense to Wojciech - the rpc transport layer doesn’t do the retires, it passes up an error, and a higher level performs retry logic, if all retries fail, it will pass up another error that the rpc failed, etc. This pattern is used throughout well designed software. If you hard using this pattern you are creating very hard to maintain software.
>
> I can't agree that in most cases the error is just returned up. I
> would agree that within a single package an error is often (but not
> always) returned up without change. But in large Go programs that
> I've seen, when error values cross package boundaries, they are almost
> always annotated with additional information.
>
>

There is no difference between

if err != nil {
return someWrappingMethod(err)
}

and

catch (SomeException e) {
throw SomeOtherException(e);
}

but, if you don’t want or can’t provide additional information you don’t need to do anything, BUT you still have to declare that your method throws the exception - this is an important benefit.

and if you have several methods or loops that throw the exception you can move the handler to outside - in Go this is plain tiresome and ugly - the blog post tries to handle this with the Scan() and ErrWriter() - again - not handling the errors as they occur (so no, Go code doesn’t ‘read’ linearly - many of the LOC may become noops interspersed with other non-noop code which is a nightmare).

Exceptions as advanced error values as more benefits:

1. the developer needs to declare what exceptions the method throws if any
- this is unbelievably valuable for efficient development and maintenance - rather than trying to read through documentation (hopefully up to date) or code to try and figure out all of the possible errors a method will return and what they mean
2. the ‘value’ of an exception contains a lot more information by default
- often no need to wrap, as the stack trace provides sufficient information, and a ‘developer label’ can easily become out of date


>> Checked exceptions are ‘error values’ with more information (e.g stack traces, causes, etc.), and can be enforced by the compiler (declared) and inspected by modern IDE’s for code complete. ‘error returns’ have none of these features. The only valid claim is that they are more expensive than error values, BUT you should not be using exceptions - or error values - for flow control. This is the fundamental problem to me with Go’s error handling - the flow control and errors are mixed leading to very difficult code to read & validate that all potential paths are handled now and in the future.
>
> My opinion, which contradicts yours, is that when the flow control is
> written down on the page, as it is in Go, the flow is easy to read,
> and it is much easier to check that all potential paths are handled.
> With exceptions the control flow is not written down, so it is easier
> to miss cases where an exception can arise. With exceptions the
> normal control flow is easier to see, but the error control flow is
> harder. And since the error control flow happens less frequently,
> it's more important to make it clearly visible to the original
> programmer and, more importantly, to the future maintainer.
>
> Again, no slam dunks here. Reasonable people can disagree. But I
> think the Go position is entirely defensible for large scale programs
> with many programmers.
>

I agree. I still believe that ‘checked exceptions’ are just ‘better error values’. As to ‘unchecked exceptions’ Go falls back on unchecked exceptions all over the place - otherwise the it would be incredibly painful for the developer to check every allocation or memory access.

Regardless, I don’t think Go can afford to rely on third-party packages for something as critical as error handling. These packages, even if limited, need to be made part of the core stdlib in order to facilitate easier long-term maintenance. These packages can be viewed as ‘exceptions light’ or ‘better errors’.

So even if Go needs to pick it now, something is better than nothing (and increased fragmentation).

R


> Ian

Ian Lance Taylor

unread,
Feb 21, 2021, 6:39:56 PM2/21/21
to robert engels, Wojciech S. Czarnecki, golang-nuts
On Sun, Feb 21, 2021 at 3:01 PM robert engels <ren...@ix.netcom.com> wrote:
>
> > On Feb 21, 2021, at 3:17 PM, Ian Lance Taylor <ia...@golang.org> wrote:
> >
> > On Sun, Feb 21, 2021 at 9:23 AM robert engels <ren...@ix.netcom.com> wrote:
> >>
> >> Can someone please explain the benefit of ‘error return’ over ‘checked exceptions’ ? I have made the point a few times and it goes to crickets - I have to believe that is because there is none, or it is difficult to communicate.
> >
> > As I wrote earlier, I believe that experience shows that it is
> > extremely difficult to write a large scale program with multiple
> > maintainers that uses checked exceptions. In practice, programs
> > always fall back to unchecked exceptions. So I would say that one
> > benefit of error return over checked exceptions is simply that error
> > returns scale to large programs and many maintainers.
> >
> I don’t think answering, “people fall back to unchecked exceptions” is an answer to “how do error returns and checked exceptions differ”.

But that isn't the question you asked. I was trying to answer the
question quoted above: "Can someone please explain the benefit of
‘error return’ over ‘checked exceptions’ ?"

My answer to this new question "how do error returns and checked
exceptions differ” would be one about visible control flow.


> I can provide examples for each case in the blog as to why it seems more 'defensive justification’ rather than evidence of being better.

We disagree.


> Exceptions as advanced error values as more benefits:
>
> 1. the developer needs to declare what exceptions the method throws if any
> - this is unbelievably valuable for efficient development and maintenance - rather than trying to read through documentation (hopefully up to date) or code to try and figure out all of the possible errors a method will return and what they mean

As I've said a couple of times, I don't think this works in actual
practice for large programs with multiple authors. Those programs
wind up falling back to unchecked exceptions, and this advantage is
lost. I don't consider this to be a controversial opinion; a number
of people have pointed this out.

> 2. the ‘value’ of an exception contains a lot more information by default
> - often no need to wrap, as the stack trace provides sufficient information, and a ‘developer label’ can easily become out of date

Sure, that is both an advantage and a disadvantage (a disadvantage
because it means that you pay a cost for every error, and that cost
may not be appropriate in all cases).

Ian

Ian Davis

unread,
Feb 21, 2021, 9:52:07 PM2/21/21
to golan...@googlegroups.com
On Sun, 21 Feb 2021, at 5:23 PM, robert engels wrote:
> Can someone please explain the benefit of ‘error return’ over ‘checked
> exceptions’ ? I have made the point a few times and it goes to crickets
> - I have to believe that is because there is none, or it is difficult
> to communicate.
>

I think since this is a Go list, the onus is on advocates of exceptions to demonstrate the benefit of checked exceptions over error return values.

Here are a couple of scenarios that I encountered recently that had logical linear flow with error returns. I'm curious how they would be improved with checked exceptions:

1) open three files for writing, closing the earlier ones if a subsequent one fails to open

2) open a file for append, falling back to creating a new writeable file if the original is read-only or doesn't exist. The new file creation may also fail due to disk errors or permissions.

I can envisage how to write them with exceptions but I'm struggling to see where it would be more succinct or easier to read.


Robert Engels

unread,
Feb 21, 2021, 10:19:55 PM2/21/21
to Ian Davis, golan...@googlegroups.com
That’s a very fair ask. I will work up some code and it will be interesting to see how much they actually differ.

I do think that the differences will be more apparent in the handling of errors during processing - eg Scan() - than during resource acquisition/initialization.

> On Feb 21, 2021, at 8:52 PM, Ian Davis <m...@iandavis.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/9f3cf023-5bb4-49da-a842-0be97904d21a%40www.fastmail.com.

Wojciech S. Czarnecki

unread,
Feb 22, 2021, 9:34:27 AM2/22/21
to robert engels, golang-nuts
Dnia 2021-02-21, o godz. 11:23:24
robert engels <ren...@ix.netcom.com> napisał(a):

> Can someone please explain the benefit of ‘error return’ over ‘checked exceptions’ ?

May you please explain the benefit of ‘checked exceptions’ over ‘return error value’?

I stated and restated benefits of 'Go-way' earlier in this thread twice. I can only add what IMO is wrong with java exceptions:

- failure context in scope is lost. Welcome back to the early C times and declare everything you might need before you open the try block.

- you have to handle failures at distance. (Or sprinkle your code with 'try-catch-es in density similar to Go's if-err-not-nil-es.)

- you need to be aware of exceptional flows within flow of exceptions - accounting for niceties that may vary from implementation to implementation of prosthetic added to alleviate practical shortcomings of the whole exceptions concept:

>>> If an exception is thrown from the try block and one or more exceptions are
>>> thrown from the try-with-resources statement, then those exceptions thrown
>>> from the try-with-resources statement are suppressed, and the exception thrown
>>> by the block is the one that is thrown by the writeToFileZipFileContents method.
>>> You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed
>>> method from the exception thrown by the try block.

Exceptions in all their glory as seen in the wild (in Oracle Java docs).


> The ‘handle where they occur claim’ is weak, as you can handle exceptions at the call site with similar LOC and constructs - but why
> when in most cases (of non-trivial software) the error value is just returned up.

IMO "when in most cases…is just returned/thrown up" is an accessory damage done to the generation exposed to exceptionally toxic fumes of convoluted exception flows ;). No offense to Robert.

> No offense to Wojciech - the rpc transport layer doesn’t do the retires, it passes up an error, and a higher level performs retry logic, if all retries fail, it will pass up another error that the rpc failed, etc. This pattern is used throughout well designed software.

> If you hard using this pattern you are creating very hard to maintain software.

I hardly use other working with std libraries. But where I can I keep the unit sealed, testable as a black-box and replaceable. Go helps with that.

> Checked exceptions are ‘error values’ with more information (e.g stack traces, causes, etc.)

This (more information everywhere) is also a fallacy stem mostly from the exceptions philosophy. I need not to build objects and move piles of data my code can not deal with. In Go I log this at the place of occurrence (for the human consumption at debug/forensics time). All what code up needs to know is whether this was a recoverable failure or an error. Try to recover if former, give up and tell supervisor if later.

> Can be enforced by the compiler (declared) and inspected by modern IDE’s for code complete.

Um... What compiler can enforce inside an 'foff/todo' (empty) catch block? Note that this empty catch can be silently placed way out of sight to not bother us in our happy flow reading experience. In Go this attitude, while it takes just two strokes, is visible in-place as a glaring lhs underscore.

> BUT you should not be using exceptions - or error values - for flow control.
WUT?

Every Java book 'exceptions' chapter starts along the 'use checked exceptions for the flow control' lines:

>>> [Joshua Bloch, Effective java] Item 40: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors. E.g. host is down, throw checked exception so the caller can either pass in a different host address or move on to something else.
>>> Item 41: Avoid unnecessary use of checked exceptions.


> This is the fundamental problem to me with Go’s error handling -
> the flow control and errors are mixed

Whether you want it or not, you must choose the path if a failure or an error happens.

> leading to very difficult code to read

(In Go) leading to code that does what it says. And a code that says explicit what it is about to do if just called piece of code did not know what to do. All in sight. The happy path can be exposed in an IDE with a fold switch [see PS].

>code difficult to validate that all potential paths are handled now and in the future.

Java compiler can validate only sad paths that it has been explicitly told of using 'throws' clause. Hardly 'all potential…and in the future'.

All in all: both java and Go ways are already established and used. Both are slowly evolving. Arguing them here over and over will not change Go usage patterns with even an iota for the simple fact Go does not support exceptions.

P.S. You, Robert, could make a gift for others who'd like to have "lucid happy path view" by amending mainstream IDEs folding code with "fold to finger" functionality. I mean, let the IDE hide every `if err != nil` {; return err }` block and display 👆 (U+1F446) next to the lhs err symbol instead:

/// Where such code
file, err := f.Open(name)
if err != nil {
return err
}
/// would display folded as
file, err👆 := f.Open(name)
///

That would be way more constructive than our yak-shaving here :)

TC,

robert engels

unread,
Feb 22, 2021, 10:23:38 AM2/22/21
to Wojciech S. Czarnecki, golang-nuts


> On Feb 22, 2021, at 8:33 AM, Wojciech S. Czarnecki <oh...@fairbe.org> wrote:
>
> Dnia 2021-02-21, o godz. 11:23:24
> robert engels <ren...@ix.netcom.com> napisał(a):
>
>> Can someone please explain the benefit of ‘error return’ over ‘checked exceptions’ ?
>
> May you please explain the benefit of ‘checked exceptions’ over ‘return error value’?
>
> I stated and restated benefits of 'Go-way' earlier in this thread twice. I can only add what IMO is wrong with java exceptions:
>
> - failure context in scope is lost. Welcome back to the early C times and declare everything you might need before you open the try block.

Much of this was alleviated with try-with-resource. Even Go’s defer is not perfect here, since the parameters are captured early - which leaves no choice but to use the same pattern in Go/Java.

>
> - you have to handle failures at distance. (Or sprinkle your code with 'try-catch-es in density similar to Go's if-err-not-nil-es.)
>
> - you need to be aware of exceptional flows within flow of exceptions - accounting for niceties that may vary from implementation to implementation of prosthetic added to alleviate practical shortcomings of the whole exceptions concept:
>
>>>> If an exception is thrown from the try block and one or more exceptions are
>>>> thrown from the try-with-resources statement, then those exceptions thrown
>>>> from the try-with-resources statement are suppressed, and the exception thrown
>>>> by the block is the one that is thrown by the writeToFileZipFileContents method.
>>>> You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed
>>>> method from the exception thrown by the try block.
>
> Exceptions in all their glory as seen in the wild (in Oracle Java docs).

This is exactly the Scan() / ErrWriter pattern as expressed in the blog post - but superior as you have a top-level exception generated - no such capability in the Go model.

>
>

I am not sure what you would expect to happen here. If the primary failure was due to a disk controller failure, and yet you try to close the file in the exception handler, you are going to get another exception. In either case, this should propagated as a ‘unable to write file’ (not an unable to close file). Now, your handler could attempt to write the file elsewhere, and again would ignore any exception (or log) any exception on the original write or close. It all depends on the semantics of the method and what it is expected to achieve.


>> The ‘handle where they occur claim’ is weak, as you can handle exceptions at the call site with similar LOC and constructs - but why
>> when in most cases (of non-trivial software) the error value is just returned up.
>
> IMO "when in most cases…is just returned/thrown up" is an accessory damage done to the generation exposed to exceptionally toxic fumes of convoluted exception flows ;). No offense to Robert.

See the Scan() and ErrWriter in the blog post - this is essentially what is done. If you have no possibly way of knowing the intermediary errors all you can do is throw up your hands because you can’t possibly know the state of the system. When this pattern is mixed with other code look out below.

>
>> No offense to Wojciech - the rpc transport layer doesn’t do the retires, it passes up an error, and a higher level performs retry logic, if all retries fail, it will pass up another error that the rpc failed, etc. This pattern is used throughout well designed software.
>
>> If you hard using this pattern you are creating very hard to maintain software.
>
> I hardly use other working with std libraries. But where I can I keep the unit sealed, testable as a black-box and replaceable. Go helps with that.
>
>> Checked exceptions are ‘error values’ with more information (e.g stack traces, causes, etc.)
>
> This (more information everywhere) is also a fallacy stem mostly from the exceptions philosophy. I need not to build objects and move piles of data my code can not deal with. In Go I log this at the place of occurrence (for the human consumption at debug/forensics time). All what code up needs to know is whether this was a recoverable failure or an error. Try to recover if former, give up and tell supervisor if later.

Ah, but then you have brittle software, or at least limited re-use. Every time you make the decision to I’ll just log here and ignore because I know best, limits the ability of some other system using the code. You can argue that code re-use is a falicy - I would not.
>
>> Can be enforced by the compiler (declared) and inspected by modern IDE’s for code complete.
>
> Um... What compiler can enforce inside an 'foff/todo' (empty) catch block? Note that this empty catch can be silently placed way out of sight to not bother us in our happy flow reading experience. In Go this attitude, while it takes just two strokes, is visible in-place as a glaring lhs underscore.
>
That is not what I am saying. How does one write a Go program and verify that all potential errors returned by a method are handled?


>> BUT you should not be using exceptions - or error values - for flow control.
> WUT?
>
> Every Java book 'exceptions' chapter starts along the 'use checked exceptions for the flow control' lines:
>
>>>> [Joshua Bloch, Effective java] Item 40: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors. E.g. host is down, throw checked exception so the caller can either pass in a different host address or move on to something else.
>>>> Item 41: Avoid unnecessary use of checked exceptions.
>
Those are not ‘flow’ in my definition. Flow is common expected conditions - like ‘end of file/stream’ - that do not require “recovery". Checked exceptions are for uncommon expected and recoverable conditions. Unchecked exceptions are for unexpected, usually unrecoverable, conditions.

>
>> This is the fundamental problem to me with Go’s error handling -
>> the flow control and errors are mixed
>
> Whether you want it or not, you must choose the path if a failure or an error happens.
>
>> leading to very difficult code to read
>
> (In Go) leading to code that does what it says. And a code that says explicit what it is about to do if just called piece of code did not know what to do. All in sight. The happy path can be exposed in an IDE with a fold switch [see PS].
>
>> code difficult to validate that all potential paths are handled now and in the future.
>
> Java compiler can validate only sad paths that it has been explicitly told of using 'throws' clause. Hardly 'all potential…and in the future'.
>
Not true, if you checked exceptions are added the called method, existing caller code will fail to compile, or run (without recompilation).

> All in all: both java and Go ways are already established and used. Both are slowly evolving. Arguing them here over and over will not change Go usage patterns with even an iota for the simple fact Go does not support exceptions.
>
> P.S. You, Robert, could make a gift for others who'd like to have "lucid happy path view" by amending mainstream IDEs folding code with "fold to finger" functionality. I mean, let the IDE hide every `if err != nil` {; return err }` block and display 👆 (U+1F446) next to the lhs err symbol instead:
>
> /// Where such code
> file, err := f.Open(name)
> if err != nil {
> return err
> }
> /// would display folded as
> file, err👆 := f.Open(name)
> ///
>
> That would be way more constructive than our yak-shaving here :)

That I agree and is also why I am open to alternatives to exceptions in Go, like forcing a method to declare all errors it might return?

robert engels

unread,
Feb 22, 2021, 10:18:50 PM2/22/21
to Ian Davis, golan...@googlegroups.com

On Feb 21, 2021, at 8:51 PM, Ian Davis <m...@iandavis.com> wrote:

On Sun, 21 Feb 2021, at 5:23 PM, robert engels wrote:
Can someone please explain the benefit of ‘error return’ over ‘checked
exceptions’ ? I have made the point a few times and it goes to crickets
- I have to believe that is because there is none, or it is difficult
to communicate.


I think since this is a Go list, the onus is on advocates of exceptions to demonstrate the benefit of checked exceptions over error return values.

Here are a couple of scenarios that I encountered recently that had logical linear flow with error returns. I'm curious how they would be improved with checked exceptions:

1) open three files for writing, closing the earlier ones if a subsequent one fails to open

The requests are a little basic and lead to trivial solutions, like this:

public static void writeFiles()  throws IOException {

try(FileOutputStream f1 = new FileOutputStream("file1");
FileOutputStream f2 = new FileOutputStream("file2");
FileOutputStream f3 = new FileOutputStream("file3")
) {
// at this point all files are open for writing and ready to be processed
// if any open failed, the other files are closed
}
}

2) open a file for append, falling back to creating a new writeable file if the original is read-only or doesn't exist. The new file creation may also fail due to disk errors or permissions.


Technically, this is just as trivial because of the way the stdlib implements this:

public static void openFile() throws IOException {

try(FileOutputStream f1 = new FileOutputStream("somefile",true);
) {
// at this the file is open for writing and will be closed when the block/method exits
}
}

But lets assume that method failed if 'open for append' failed if the file didn’t exist:

class Test {
private static FileOutputStream openAppendOrCreate(String name) throws FileNotFoundException {
try {
return new FileOutputStream(name,true);
} catch(FileNotFoundException e){
return new FileOutputStream(name);
}
}

public static void openFile() throws IOException {

try(FileOutputStream f1 = openAppendOrCreate("somefile");
) {
// at this the file is open for writing and will be closed when the block exits
}
}
}
Still, in a significant system and this being a core operation I wouldn’t rely on so much stdlib convenience/behavior and would instead use more low-level methods that take a File, checking for existence and writability, etc. but the basic pattern of wrapping ‘resource creation’ into a method to improve readability and keep the try-with-resource blocks simple.

If you need to delete a file in addition to closing when the block exits you are better off using a ‘resource creation’ method that creates a wrapper stream that deletes on close. Or you could use the various ‘create temp’ file methods and ‘delete on exit’ and let the platform handle the clean-up.

I can envisage how to write them with exceptions but I'm struggling to see where it would be more succinct or easier to read.


--
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.

Nigel Tao

unread,
Feb 23, 2021, 7:03:11 AM2/23/21
to golang-nuts
If you're proposing exceptions, be aware of how complicated the control flow for correct handling can be. Look for what follows `The statement "RESULT = yield from EXPR" is semantically equivalent to...` in https://www.python.org/dev/peps/pep-0380/

Robert Engels

unread,
Feb 23, 2021, 8:42:25 AM2/23/21
to Nigel Tao, golang-nuts
The complexity there is from generators + exceptions. I am not proposing generators for Go as it doesn’t need it. 

On Feb 23, 2021, at 6:03 AM, Nigel Tao <nige...@golang.org> wrote:


If you're proposing exceptions, be aware of how complicated the control flow for correct handling can be. Look for what follows `The statement "RESULT = yield from EXPR" is semantically equivalent to...` in https://www.python.org/dev/peps/pep-0380/

--
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.

da...@suarezhouse.net

unread,
Feb 23, 2021, 2:02:13 PM2/23/21
to golang-nuts
This has been interesting to watch as I too have somehow been "converted" from java style exceptions to current go-style error flow as preference.  In the first example you just shared, I think if you elaborate the comments, "at this point all files are open, if any fail, others are closed", you may see "logic" in your exception handler like "if f1 != nil f1.close; if f2 != null f2.close(), etc...".  Having an additional logic point seems "less clean" IMHO than handling it one file at a time as it happens.  If you wanted to add some context around what is special in file 1 - 3 in that example you could also include it in line vs. as a separate logic tree in the exception handler "config file did not load" vs. "data file", etc..  

I had a different similar problem to the multi-file example where "missing" is normal and while solveable if you change your logic to first check file.exists() not having to add a check for something you already get in the error makes the logic simpler on the page.

The translation I have become used to is similar to how you started, e.g. Runtime Exception = Panic, Checked Exception = Error.  The one part that I do like in what you mention is the explicit errors in the definition which become part of documentation.  That does force some good habits that need to be done manually today but can potentially be as simple as a vet check on a future comment style if missing and returns an error type.  

From a value standpoint, the incremental benefit of changes to error seems low compared to the other big changes like generics.

My .02 if it helps the conversation at all.

Sincerely,
David  

robert engels

unread,
Feb 23, 2021, 5:30:46 PM2/23/21
to da...@suarezhouse.net, golang-nuts
I’ll only add to this, there is no reason to close in the exception handler - it is automatic by the try-with-resource. The only reason to check null is if you needed to “do something else” based on that state.

I think a better pattern with the “something else” is to always use standard wrappers on the common resources so that try-with-resource is as simple as possible.

Michael MacInnis

unread,
Feb 25, 2021, 8:36:37 PM2/25/21
to golang-nuts
I realize that this thread ended up diverging into a wider discussion of exceptions and error handling in general but I wanted mention one of the problems I discovered and maybe a potential solution for others who may be tempted to play around with something like this.

I originally thought that one of bigger problems with (ab)using panic and recover for control flow and trying to wrap this up in a package would be forgetting to defer the function performing the recover. As mentioned previously I think it's possible to structure things so that it is harder to make this mistake and I think a linter could also be written to catch these cases.

But I think the bigger problem is the function that panics being used after the function with the deferred recover exits or in a different goroutine.

It looks like the Go compiler's escape analysis can be used to detect this though. At least currently. With an inline definition of check and a deferred recover that looks like this:

    var check func(error)
    {
        pnc := false

        check = func(ce error) { // watch for "func literal escapes to heap"
            if ce != nil {
                err = ce

                // Only panic if we haven't previously.
                if !pnc {
                    pnc = true
                    panic("escaping")
                }
            }
        }

        defer func() {
            if pnc {
                _ = recover()
            }
        }()
    }

    // Code that uses check.

When compiled with `go build -gcflags -m` if the Go compiler says that the func literal assigned to check escapes to the heap there's a good chance it is being used in a way that won't end well. It even catches uses like this:

    c := make(chan struct{})

    go func() {
        check(nil)

        close(c)
    }()

    <-c

where check doesn't out live the function with the deferred recover but it is being used in a different goroutine. So, at least currently, it looks like the compiler can be used to check for unsafe uses of a function like this.

What I'm wondering is are there cases that the compiler won't catch and is this likely to stop working in some future release where, in a block of code like the one above, the compiler will decide that the function doesn't need to escape to the heap?

Michael.
Reply all
Reply to author
Forward
0 new messages