Skip to content

proposal: fail: new package for checking errors #58631

Open
@beoran

Description

@beoran

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Proposalerror-handlingLanguage & library change proposals that are about error handling.

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions