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: Add generic fail package to standard library for error handling #58631

Open
beoran opened this issue Feb 22, 2023 · 6 comments
Open
Labels
error-handling Language & library change proposals that are about error handling. Proposal
Milestone

Comments

@beoran
Copy link

beoran commented Feb 22, 2023

I have been trying out generics for error handling, and I found that it might be a good approach to consider, in stead of changing the language. Therefore I propose adding a new package to the standard library, named "fail", for now, which does such error handling, using fail.Check() and fail.Save() as the main API.

The interesting points about this approach are that error handlers can be defined and reused easily, that even the Check can be reused easily, though a combination of higher order functions, generics and panic/rescue, and that it is all very simple to use and to implement.

The weakest points are the need for Check2, Check3 ... functions for that return more results than just one and an error, although these are relatively rare in Go, that it requires named return values, and the fact that this does use panics behind the scenes, although this can also be seen as a benefit in a sense.

In a later stage then, if deemed necessary, some of these functions could be made built-in functions with the same semantics, but that can be considered after this proposal.

Below is a sketch of how this could work. It was tested only slightly, and probably needs to be improved a lot.

https://go.dev/play/p/jQRLjq-0pw0


import (
	"fail/fail"
	"fmt"
)

func div(a, b int) (int, error) {
	if b == 0 {
		return 0, fmt.Errorf("div: division by 0: %d / %d", a, b)
	}
	return a / b, nil
}

func mod(a, b int) (int, error) {
	if b == 0 {
		return 0, fmt.Errorf("mod: division by 0: %d / %d", a, b)
	}
	return a % b, nil
}

func maths() (r int, rerr error) {
	save := fail.Save(&rerr)
	defer save()
	check := fail.Check[int](fail.Decorate("maths error: %w"), fail.Print())
	r = check(div(12, 3))
	fmt.Printf("%d\n", r)
	r = check(mod(12, 7))
	fmt.Printf("%d\n", r)
	r = check(div(r, 0))
	fmt.Printf("%d\n", r)
	return r, nil
}

func main() {
	defer fail.Save(nil)
	decorator := fail.Decorate("main: error: %w")
	handler := fail.Print()

	fail.Check[int](decorator, handler, fail.Exit(7))(maths())
}
-- go.mod --
module fail
-- fail/fail.go --
package fail

import (
	"fmt"
	"os"
)

// checked are errors that this package can catch with Save or Catch
type checked error

// Check returns a function that will check the results from a function
// which returns (Result, error). If error is nil, Result is returned normally.
// If error not is nil, the handlers will be called one by one.
// If one of the handlers returns nil, the error handling stops and the
// check function will return the zero value of Result.
// Otherwise a panic will be raised which should be handled with
// [Catch] or [Save] .
func Check[Result any](handlers ...func(error) error) func(res Result, err error) Result {
	return func(res Result, err error) Result {
		if err != nil {
			for _, handler := range handlers {
				err = handler(err)
				if err == nil {
					var zero Result
					return zero
				}
			}
			panic(checked(err))
		}
		return res
	}
}

// Check2 returns a function that will check the results from a function
// which returns (Result1, Result2 error). If error is nil, Result1 and Result2
// are returned normally.
// If error not is nil, the handlers will be called one by one.
// If one of the handlers returns nil, the error handling stops and the
// check function will return the zero value of Result.
// Otherwise a panic will be raised which should be handled with
// [Catch] or [Save] .
func Check2[Result1, Result2 any](handlers ...func(error) error) func(res1 Result1, res2 Result2, err error) (Result1, Result2) {
	return func(res1 Result1, res2 Result2, err error) (Result1, Result2) {
		if err != nil {
			for _, handler := range handlers {
				err = handler(err)
				if err == nil {
					var zero1 Result1
					var zero2 Result2
					return zero1, zero2
				}
			}
			panic(checked(err))
		}
		return res1, res2
	}
}

// CheckErr is like Check but in case the function returns only an error
// and no values.
func CheckErr(handlers ...func(error) error) func(err error) {
	return func(err error) {
		if err != nil {
			for _, handler := range handlers {
				err = handler(err)
				if err == nil {
					return
				}
			}
			panic(checked(err))
		}
	}
}

// Catch catches the error in case it was raied by Check().
// Should be used only in a defer clause as follows:
//
// defer func() { err = Catch(recover()) } ()
func Catch(caught any) error {
	aid := caught
	if aid == nil {
		return nil
	}
	if err, ok := aid.(checked); ok {
		return (error)(err)
	}
	// Panic again if not a Checked error
	panic(aid)
}

// Save catches the error if raised by Check.
// I rerr is nil, nothing else happens, if it is not nil, the caught error
// will be stored in rerr
// Should be used only in a defer clase as follows:
// defer Save(&rerr)()
func Save(rerr *error) func() {
	return func() {
		err := Catch(recover())
		if rerr != nil {
			*rerr = err
		}
	}
}

// Print returns an error handler that prints the error message to standard
// error if there is one. Print appends a newline.
func Print() func(err error) error {
	return func(err error) error {
		if err != nil {
			fmt.Fprintf(os.Stderr, "%s\n", err)
		}
		return err
	}
}

// Decorate returns an error handler that decorates the error if there is one.
// The format parameter must be a valid fmt.Printf format,
// which contains a %w.
func Decorate(format string) func(err error) error {
	return func(err error) error {
		if err != nil {
			return fmt.Errorf(format, err)
		}
		return err
	}
}

// Exit returns an error handler that calls os.Exit() if there is an error.
func Exit(exit int) func(err error) error {
	return func(err error) error {
		if err != nil {
			os.Exit(exit)
		}
		return err
	}
}
@gopherbot gopherbot added this to the Proposal milestone Feb 22, 2023
@beoran
Copy link
Author

beoran commented Feb 22, 2023

@gopherbot add error-handling

@gopherbot gopherbot added the error-handling Language & library change proposals that are about error handling. label Feb 22, 2023
@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Feb 22, 2023
@ianlancetaylor
Copy link
Contributor

For a pure library like this we would typically suggest that we try it out as an external package first and see what kind of adoption it gets.

@earthboundkid
Copy link
Contributor

Have you seen @dsnet's try? https://pkg.go.dev/github.com/dsnet/try This seems very similar.

@beoran
Copy link
Author

beoran commented Feb 23, 2023

@ianlancetaylor While I would normally agree with https://go.dev/doc/faq#x_in_std, seeing that error handling in Go is a hot topic, which has been looking for a solution for a decade, I am proposing we skip the step of the external package and get started with a design and then try it out in x/ as an experimental package.

I don't really care about the name or the details of the implementation, the idea is to use existing language features including generics to solve the Go error handling problem in a standardized way. Seeing how we now are getting slog for structured logging, which was also long overdue, I think it would be a good idea to cut to the chase, so to speak.

Especially because it looks like we will not add any new language features to Go for error handling. So I thought the standard library approach would be best then

However I would also not object if this proposal were to be put on hold until there are a few similar packages like this.

@carlmjohnson I looked at it and while it is similar, my example has the benefit that the both the check functionand the error handlers are returned as higher order functions and can be reused. And also that I choose for more readable function names. But one reason I made this proposal is for everyone to help out with the design. What I have is just something I cobbled together in a few hours.

@SealOfTime
Copy link

Error handling happens a lot and is required to be efficient almost every time. I don't believe, that generics spread over closures and panics with their stack unwinding is the best tool for the job.

@adonovan
Copy link
Member

I am proposing we skip the step of the external package and get started with a design and then try it out in x/ as an experimental package.

I think Ian is proposing that you don't skip that step. Something as important as this should be carefully evaluated within a single large project (or company) before it pretends to be a good solution for everyone. There's no need to involve the x/ repos.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
error-handling Language & library change proposals that are about error handling. Proposal
Projects
Status: Incoming
Development

No branches or pull requests

6 participants