Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Proposal: Go2: Error checking and handling inspired on Switch/Case statements #35086

Closed
gbitten opened this issue Oct 22, 2019 · 34 comments
Closed
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@gbitten
Copy link

gbitten commented Oct 22, 2019

Proposal: Go2: Error checking and handling inspired on Switch/Case statements

  • Author: Gustavo Bittencourt

Summary

This proposal tries to address an aspect of Go programs which "have too much code checking errors and not enough code handling them" (Error Handling — Problem Overview). To solve this, the proposal is inspired by Switch/Case statements in Go.

Introduction

This proposal creates the new statement, handle, that defines a scope where one variable is checked and handled as soon there is assignment operation with that variable. That variable is called "handled variable". For example, the following code is how Go handles errors:

func printSum(a, b string) error {
	x, err := strconv.Atoi(a)
	if err != nil {
		return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
	}
	y, err := strconv.Atoi(b)
	if err != nil {
		return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
	}
	fmt.Println("result:", x+y)
	return nil
}

In this proposal, the function could be the following:

func printSum(a, b string) error {
	var err error
	handle err {
		x, err := strconv.Atoi(a)
		y, err := strconv.Atoi(b)
		fmt.Println("result:", x + y)
	case err != nil:
		return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
	}
	return nil
}

The handle statement defines a handled variable (err in the above example) and a scope where this variable is handled. In the handle scope, each time the handled variable is in the left-hand side of an assignment operation, immediately after that assignment operation, it will be executed the checks defined in the case statements.

The case statements are evaluated top-to-bottom; the first one that is true, triggers the execution of the statements of the associated case; the other cases are skipped. If no case matches no one is executed. There is no "default" case.

Defining error handled-variable in the handle statement

The handled variable can be defined in the handle statement, so the code can be even shorter.

func printSum(a, b string) error {
	handle err error {  // <-- the variable 'err' is defined here
		x, err := strconv.Atoi(a)
		y, err := strconv.Atoi(b)
		fmt.Println("result:", x + y)
	case err != nil:
		return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
	}
	return nil
}

Example: Error handling with defer statement

In Golang:

func CopyFile(src, dst string) error {
	r, err := os.Open(src)
	if err != nil {
		return err
	}
	defer r.Close()
	w, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer w.Close()
	if _, err := io.Copy(w, r); err != nil {
		return err
	}
	if err := w.Close(); err != nil {
		return err
	}
}

In this proposal:

func CopyFile(src, dst string) error {
	handle err Error {
		r, err := os.Open(src)
		defer r.Close()
		w, err := os.Create(dst)
		defer w.Close()
		_, err = io.Copy(w, r)
		err = w.Close()
	case err != nil:
		return err
	}
}

Example: Error handling without return

In Golang:

func main() {
	hex, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		log.Fatal(err)
	}
	data, err := parseHexdump(string(hex))
	if err != nil {
		log.Fatal(err)
	}
	os.Stdout.Write(data)
}

In this proposal:

func main() {
	handle err Error {
		hex, err := ioutil.ReadAll(os.Stdin)
		data, err := parseHexdump(string(hex))
		os.Stdout.Write(data)
	case err != nil:
		log.Fatal(err)
	}
}

Handling non-error variable

It is possible to handle non-error variables, like:

func doSomething() error {
	handle i int {
		i = func1()
		i = func2()
	case i == -1:
		return fmt.Errorf("Error: out-of-bound")
	case i == -2:
		return fmt.Errorf("Error: not a number")
	}
}

Nested handle scopes

There are some hard decisions to make the nested handle scopes works properly. So, this proposal suggests to forbid nested handle scopes.

@gopherbot gopherbot added this to the Proposal milestone Oct 22, 2019
@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Oct 22, 2019
@cep21
Copy link
Contributor

cep21 commented Oct 23, 2019

What happens if err is modified by a goroutine to not be nil? What if err is modified while not on the left hand side? What if err is shadowed?

@gbitten
Copy link
Author

gbitten commented Oct 23, 2019

What happens if err is modified by a goroutine to not be nil? What if err is modified while not on the left hand side? What if err is shadowed?

Nothing. Case statements are trigger only when the handled variable appears in the left-hand side of an assignment operation in the handle scope.

@cyrusaf
Copy link

cyrusaf commented Oct 23, 2019

As much as this simplifies the error handling flow, it also hinders readability. One of the biggest advantages of handling errors after each function is that you can scan a file and understand exactly which functions might return errors and how those errors are handled.

@raff
Copy link

raff commented Oct 23, 2019

How does this handle the "locality" of the error ?

             ...
	x, err := strconv.Atoi(a)
	y, err := strconv.Atoi(b)

How do I know if I got the error in the first statement or the second ?

I guess in most cases one would be calling different methods that return different errors (that may or may not be easy to reconnect to the statement that generated the error) but at least in this particular example that wouldn't work.

@cep21
Copy link
Contributor

cep21 commented Oct 23, 2019

What if err is on the left hand side inside of an inline defer function?

@raff
Copy link

raff commented Oct 23, 2019

The syntax is really odd, with statements before the case. Is not really comparable to the switch/case statements. It looks more like try/catch where you didn't want to use the try/catch keywords :)

@DGKSK8LIFE
Copy link

there's nothing wrong with go error handling, don't ruin things for the rest of us

@mvpmvh
Copy link

mvpmvh commented Oct 23, 2019

I feel like people really don't like (or grasp) the concept that errors are just values. Would you do this pattern for handling bools, ints, strings, etc? I certainly wouldn't create some block within a function that handled those values. Why are we trying to treat errors as something special?

@dominikbraun
Copy link

Why are we trying to treat errors as something special?

Because other languages like Java tought us to do so. However, this is neither Go-ish nor idiomatic. Errors are a very common thing to happen...

@gbitten
Copy link
Author

gbitten commented Oct 23, 2019

"This proposal tries to address an aspect of Go programs which "have too much code checking errors and not enough code handling them" (Error Handling — Problem Overview)."

I didn't create that statement. It is perfertly ok to disagree of the statement and the proposal. But please, don't judge me.

@gbitten
Copy link
Author

gbitten commented Oct 23, 2019

How do I know if I got the error in the first statement or the second ?

You don't know. This proposal doesn't apply for that user case.

@gbitten
Copy link
Author

gbitten commented Oct 23, 2019

What if err is on the left hand side inside of an inline defer function?

An inline defer function runs outside of the handle scope, so the case statements are not trigged.

@gbitten gbitten closed this as completed Oct 23, 2019
@gbitten gbitten reopened this Oct 23, 2019
@WillRubin
Copy link

there's nothing wrong with go error handling, don't ruin things for the rest of us

There being "nothing wrong with go error handling" is completely subjective. Myself and some other gophers believe Go's current error handling it is too verbose and distracting and that Go would benefit from a change here.

This proposal appears to add an additional way to handle errors but doesn't appear to force you to change the way you currently code. It might not be an ideal solution but it's interesting enough to read the proposal and see the discussion. It's definitely not rising to the level of "ruining" the language even for those who oppose changing Go.

@nomad-software
Copy link

This proposal is horrible. It doesn't look or feel like Go and it hides the origin of the error. Errors are just values, treat them as such.

@conilas
Copy link

conilas commented Oct 23, 2019

I feel like people really don't like (or grasp) the concept that errors are just values. Would you do this pattern for handling bools, ints, strings, etc? I certainly wouldn't create some block within a function that handled those values. Why are we trying to treat errors as something special?

I do not agree with your statement. Errors are something special. Errors are, in a sense, a representation of a non-complete computation.

@ddspog
Copy link

ddspog commented Oct 23, 2019

I see value on your proposal. Try and make it using go code, creating a module with handle as a variadic function, receiving any argument, and then checking on all errors founded.

@AndrusGerman
Copy link

How can I know if func1 or func2 failed even in both?

func doSomething() error {
	handle i int {
		i = func1()
		i = func2()
	case i == -1:
		return fmt.Errorf("Error: out-of-bound")
	case i == -2:
		return fmt.Errorf("Error: not a number")
	}
}

@gbitten
Copy link
Author

gbitten commented Oct 23, 2019

How can I know if func1 or func2 failed even in both?

You can't. This proposal tries to address an user case which has an homogeneous error handling.

@pbar1
Copy link

pbar1 commented Oct 23, 2019

Is it really worth it to include as a language feature then, just for that use case? Even then, it makes a program using this proposal harder to reason about in error states.

@nomad-software
Copy link

I feel like people really don't like (or grasp) the concept that errors are just values. Would you do this pattern for handling bools, ints, strings, etc? I certainly wouldn't create some block within a function that handled those values. Why are we trying to treat errors as something special?

I do not agree with your statement. Errors are something special. Errors are, in a sense, a representation of a non-complete computation.

@conilas et al please read this: https://blog.golang.org/errors-are-values

@raff
Copy link

raff commented Oct 23, 2019

In my opinion the original Go2 proposal for error handling didn't go through because, like this, only solved one of the use cases that people wanted (reduce the boiler plate).

Turns out developers wanted more: reduce the boilerplate, wrap errors, etc.

Before coming up with another proposal for error handling we should really first decide what is the minimal set of expectations that a proposal for new error handling should satisfy.

@DGKSK8LIFE
Copy link

Myself and some other gophers believe Go's current error handling it is too verbose and distracting and that Go would benefit from a change here.

Why/how do you think go's err handling is too verbose?

@DGKSK8LIFE
Copy link

This proposal appears to add an additional way to handle errors but doesn't appear to force you to change the way

Right, but I think we should always reference go's design philosophy before potentially adding features; maybe we can poll?

@WillRubin
Copy link

Why/how do you think go's err handling is too verbose?

It forces the same wordy construct over and over. It's my opinion, of course, but dropping
if err != nil {
return err
}
and its variations all through the happy path harms readability and understanding.

If I'm not going to be able to do anything about an error or exception but essentially pass it up the call hierarchy then I'd rather have something that's a bit off happy path and would leave the non-exceptional logic intact. If you believe demarking each place where a potential error can occur is important to reading the code then you're probably more inline with the Go's original intent. I think the original intent was an interesting idea (even ideology) but hasn't turned out as well in practice. I'm not opposed to changing the language at this level to add more practical options.

Note: there are already a couple patterns that can help with this but they're certainly not as elegant as what could be achieved by integrating a solution within Go.

@DGKSK8LIFE
Copy link

Note: there are already a couple patterns that can help with this but they're certainly not as elegant as what could be achieved by integrating a solution within Go.

What do you think about the current ubiquitous try, catch, finally err handling method in general?

@WillRubin
Copy link

... I think we should always reference go's design philosophy before potentially adding features ...

If you think that Go's design philosophy was correct and works well for your uses then I agree. If you believe, like I do, that Go's design philosophy was good but not perfect and could now use a bit of correction then adding or changing features seems appropriate.

Go's philosophy for errors was that they're values to be be coded against like any other conditional return value. This turns out to be correct some of the time but not all the time. Or maybe all of the time if you're really wedded to wanting them to be. I think Go would benefit from considering them from additional perspectives too.

@DGKSK8LIFE
Copy link

yeah

@WillRubin
Copy link

What do you think about the current ubiquitous try, catch, finally err handling method in general?

I think try/catch is excellent for certain types of errors. Database errors in particular but errors where you need to ensure resources are cleaned up work well with try/catch.

Try/catch is nice because it sits a bit above the happy path. One try can hold a half dozen error points so you often don't need to think about the error conditions at all in that section of your code. If you hit the catch you get the error so you can inspect if if that's of value. You also can pick up the stack trace too and use it or pass it along as appropriate. There are a couple ways to simulate try/catch in Go already, but they involve defer and defer can suffer from scoping and timing edge cases. I don't think they're as clear.

That said, try/catch is horrible for so many classes of errors I can understand why Go's designers wanted to try another way. For me, I'm not opposed to having multiple ways of handling errors in Go. "x, err :=" is nice for many (most) situations. But I would like it if a less verbose version were available in Go. Errors/exceptions are diverse enough I don't think a one size fits all solution is going to be found in the Algol language tree.

@WillRubin
Copy link

WillRubin commented Oct 24, 2019

FWIW, quite some time ago I came up with an error handling syntax that (1) keeps all existing code working and (2) handles about 40-50% of the boilerplate error code in my code. Two (or three) language changes.

It would reduce this

if x, err := a(); err != nil {
  return err
}
if y, err := b(); err != nil {
  return err
}
if z, err := c(); err != nil {
  return err
}

to this

x, err := a()
y, err := b()
z, err := c()

Here's the gist:

package main

import (
    "fmt"
    "log"
)

// could be a type signature
type ErrorHandler1 func(err error) error

// or could be an interface
type ErrorHandler2 interface {
    Handler(err error) error
}

func liveErrFunc(err error) error {
    return2 err // HERE'S THE FIRST CHANGE. Must return from caller's scope, not just this scope.
}

func debugErrFunc(err error) error {
    log.Println("Now we're logging.")
    return2 err  // yes, it's a horrible keyword name.
}


func main() {
    fmt.Println("Hello World")

    var anErrorHandler ErrorHandler1 // using the type directly
    anErrorHandler = liveErrFunc

    for _, num := range []int{1, 2} {
        if num == 2 {
            anErrorHandler = debugErrFunc
        }
        result, anErrorHandler := erroringCall()  // HERE'S THE OTHER CHANGE. 

        // We're allowed to put a function pointer variable in place of the error variable here.
        // The compiler adjusts accordingly for the new syntax.
        // That's it. Existing code works. New code can be abstracted away and even changed at runtime.
    }
}

With one more change you could allow passing a string into the error handler at the call site:

x, errHandler("I'm a unique error point: %v", err) := erroringCall()

@DGKSK8LIFE
Copy link

FWIW, quite some time ago I came up with an error handling syntax that (1) keeps all existing code working and (2) handles about 40-50% of the boilerplate error code in my code. Two (or three) language changes.

It would reduce this

if x, err := a(); err != nil {
  return err
}
if y, err := b(); err != nil {
  return err
}
if z, err := c(); err != nil {
  return err
}

to this

x, err := a()
y, err := b()
z, err := c()

Here's the gist:

package main

import (
    "fmt"
    "log"
)

// could be a type signature
type ErrorHandler1 func(err error) error

// or could be an interface
type ErrorHandler2 interface {
    Handler(err error) error
}

func liveErrFunc(err error) error {
    return2 err // HERE'S THE FIRST CHANGE. Must return from caller's scope, not just this scope.
}

func debugErrFunc(err error) error {
    log.Println("Now we're logging.")
    return2 err  // yes, it's a horrible keyword name.
}


func main() {
    fmt.Println("Hello World")

    var anErrorHandler ErrorHandler1 // using the type directly
    anErrorHandler = liveErrFunc

    for _, num := range []int{1, 2} {
        if num == 2 {
            anErrorHandler = debugErrFunc
        }
        result, anErrorHandler := erroringCall()  // HERE'S THE OTHER CHANGE. 

        // We're allowed to put a function pointer variable in place of the error variable here.
        // The compiler adjusts accordingly for the new syntax.
        // That's it. Existing code works. New code can be abstracted away and even changed at runtime.
    }
}

With one more change you could allow passing a string into the error handler at the call site:

x, errHandler("I'm a unique error point: %v", err) := erroringCall()

that sounds good!

@ianlancetaylor
Copy link
Contributor

The proposed handle statement looks vaguely like a switch statement, with a similar syntax and use of the keyword case. But the execution is nothing like a switch statement. That seems potentially confusing.

@bradfitz bradfitz added the error-handling Language & library change proposals that are about error handling. label Oct 29, 2019
@ZolAnder85
Copy link

ZolAnder85 commented Nov 19, 2019

If I understand correctly, it would execute the statements until one of the conditions is met.
A more general approach would be to introduce a for_all_statement (with better name):

for_all_statement { // do every statement inside
    x, err := a()
    y, err := b()
} // this is basically a block

var err error
for_all_statment err == nil { // check condition after every statment
    x, err := a()
    y, err := b()
}

for_all_statment err error; err == nil {
    x, err := a()
    y, err := b()
}

for_all_statment i := 0; i < 100 { // can be used for other things too
    i += countA()
    i += countB()
    i += countC()
}

for_all_statment err error; err == nil {
    x, err := a()
    y, err := b()
} else { // what to do if block got broken
    fmt.Println(err)
}

// maybe this would be nice too
var x int32
var err error
for_all_statment {
    x, err = a()
    x, err = b()
} do { // placeholder name
    if err == nil {
        if x == 0 {
            fmt.Println("success but still breaking")
            break // optionally with label
        }
    } else {
        fmt.Println("breaking due to error")
        break
    }
}

@ianlancetaylor
Copy link
Contributor

According to emoji voting, there is no strong support for this change. There are several comments above pointing out difficulties. For these reasons, this is a likely decline. Leaving open for four weeks for final comments.

@griesemer
Copy link
Contributor

No final comments. Closing.

@golang golang locked and limited conversation to collaborators Dec 16, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests