err2

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 16, 2022 License: MIT Imports: 7 Imported by: 164

README

err2

The package provides simple helper functions for automatic error propagation.

go get github.com/lainio/err2

Error Propagation

The current version of Go tends to produce too much error checking and too little error handling. This package helps us fix that.

  1. It helps to declare error handlers with defer.
  2. It helps to check and transport errors to the nearest (the defer-stack) error handler.

You can use both of them or just the other. However, if you use err2 for error checks you must remember use Go's recover() by yourself, or your error isn't transformed to an error.

Error handling

Package err2 relies on Go's declarative programming structure defer. The err2 helps to set deferred functions (error handlers) which are only called if err != nil.

In every function which uses err2 for error-checking should have at least one error handler. If there are no error handlers and error occurs the current function panics. However, if function above in the call stack has err2 error handler it will catch the error. The panicking for the errors at the start of the development is far better than not checking errors at all.

This is the simplest err2 error handler

defer err2.Return(&err)

which is the helper handler for cases that don't need to annotate the error. If you need to annotate the error you can use either Annotate or Returnf.

Error Handler

The err2.Handle is a helper function to add actual error handlers which are called only if an error has occurred. In most real-world cases, we have multiple error checks and only one or just a few error handlers. However, you can have as many error handlers per function as you need.

Read the package documentation for more information.

Error checks

The err2 provides convenient helpers to check the errors. Since the Go 1.18 we use generics to have fast and convenient error checking.

For example, instead of

b, err := ioutil.ReadAll(r)
if err != nil {
        return err
}
...

we can call

b := try.To1(ioutil.ReadAll(r))
...

but not without an error handler (Return, Annotate, Handle) or it just panics your app if you don't have a recovery call in the goroutines calls stack. However, you can put your error handlers where ever you want in your call stack. That can be handy in the internal packages and certain types of algorithms.

Type Helpers

These are now obsolete: when Go2 generics are out we can replace all of these with generics.

Filters for non-errors like io.EOF

When error values are used to transport some other information instead of actual errors we have functions like FilterTry and even TryEOF for convenience.

With these you can write code where error is translated to boolean value:

	notExist := err2.FilterTry(plugin.ErrNotExist, r2.err)

	// real errors are cought and the returned boolean tells if value
	// dosen't exist returnend as `plugin.ErrNotExist`

For more information see the examples of both functions.

Assertion (design by contract)

The assert package has been since version 0.6. The package is meant to be used for design by contract -type of development where you set preconditions for your functions. It's not meant to replace normal error checking but speed up incremental hacking cycle. That's the reason why default mode (var D Asserter) is to panic. By panicking developer get immediate and proper feedback which allows cleanup the code and APIs before actual production release.

func marshalAttestedCredentialData(json []byte, data *protocol.AuthenticatorData) []byte {
	assert.D.EqualInt(len(data.AttData.AAGUID), 16, "wrong AAGUID length")
	assert.D.NotEmpty(data.AttData.CredentialID, "empty credential id")
	assert.D.NotEmpty(data.AttData.CredentialPublicKey, "empty credential public key")
	...

Previous code block shows the use of the asserter (D) for developing. If any of the assertion fails, code panics. These type of assertions can be used without help of the err2 package.

During the software development lifecycle it isn't all the time crystal clear what are preconditions for a programmer and what should be translated to end-user errors as well. That's why assert package uses concept called Asserter to have different type of asserter for different phases of a software project.

The following code block is a sample where production time asserter is used to generate proper error messages.

func (ac *Cmd) Validate() (err error) {
	defer err2.Return(&err)

	assert.P.NotEmpty(ac.SubCmd, "sub command needed")
	assert.P.Truef(ac.SubCmd == "register" || ac.SubCmd == "login",
		"wrong sub command: %s: want: register|login", ac.SubCmd)
	assert.P.NotEmpty(ac.UserName, "user name needed")
	assert.P.NotEmpty(ac.Url, "connection URL cannot be empty")
	assert.P.NotEmpty(ac.AAGUID, "authenticator ID needed")
	assert.P.NotEmpty(ac.Key, "master key needed")

	return nil
}

When asserts are used to generate end-user error messages instead of immediate panics, err2 handlers are needed to translate asserts to errors in convenient way. That's the reason we decided to build assert as a sub package of err2 even there are no real dependencies between them. See the assert packages own documentation and examples for more information.

Background

err2 implements similar error handling mechanism as drafted in the original check/handle proposal. The package does it by using internally panic/recovery, which some might think isn't perfect. We have run many benchmarks to try to minimise the performance penalty this kind of mechanism might bring. We have focused on the happy path analyses. If the performance of the error path is essential, don't use this mechanism presented here. For happy paths by using err2.Check type helper variables there seems to be no performance penalty.

However, the mandatory use of the defer might prevent some code optimisations like function inlining. If you have a performance-critical use case, we recommend you to write performance tests to measure the effect.

The original goal was to make it possible to write similar code than proposed Go2 error handling would allow and do it right now. The goal was well aligned with the latest Go2 proposal where it would bring a try macro and let the error handling be implemented in defer blocks. The try-proposal was put on the hold or cancelled at its latest form. However, we have learned that using panics for early-stage error transport isn't a bad thing but opposite. It seems to help to draft algorithms.

Learnings by so far

We have used the err2 and assert packages in several internal projects. The results have been so far very encouraging:

  • If you forget to use handler, but you use checks from the package, you will get panics if an error occurs. That is much better than getting unrelated panic somewhere else in the code later. There have also been cases when code reports error correctly because the 'upper' handler catches it.

  • Because the use of err2.Annotate is so relatively easy, error messages much better and informative.

  • When error handling is based on the actual error handlers, code changes have been much easier.

  • You don't seem to need '%w' wrapping. See the Go's official blog post what are cons of that.

Do not wrap an error when doing so would expose implementation details.

Roadmap

Version history:

  • 0.1, first draft (Summer 2019)
  • 0.2, code generation for type helpers
  • 0.3, Returnf added, not use own transport type anymore but just error
  • 0.4, Documentation update
  • 0.5, Go modules are in use now
  • 0.6.1, assert package added, and new type helpers (current)
  • 0.7.0 filter functions for the cases where errors aren't real errors like io.EOF
  • 0.8.0 try.To() & assert.That(), etc. functions with the help of the generics

Documentation

Overview

Package err2 provides three main functionality:

  1. err2 package includes helper functions for error handling.
  2. try package is for error checking
  3. assert package is for design-by-contract and preconditions

The traditional error handling idiom in Go is roughly akin to

if err != nil {
	return err
}

which applied recursively. That leads to code smells: redundancy, noise&verbose, or suppressed checks. The err2 package drives programmers more to focus on error handling rather than checking errors. We think that checks should be so easy (help of the declarative control structures) that we never forget them.

try.To1(io.Copy(w, r))

Error checks

The err2/try provides convenient helpers to check the errors. For example, instead of

b, err := ioutil.ReadAll(r)
if err != nil {
	return err
}

we can write

b := try.To1(ioutil.ReadAll(r))

but not without the handler.

Error handling

Package err2 relies on error handlers. In every function which uses err2 or try package for error-checking has to have at least one error handler. If there are no error handlers and error occurs it panics. Nevertheless, we think that panicking for the errors during the development is much better than not checking errors at all. However, if the call stack includes any err2 error handlers like err2.Handle() the error is handled there where the handler is saved to defer stack.

The handler for the previous sample is

defer err2.Return(&err)

which is the helper handler for cases that don't annotate errors. err2.Handle is a helper function to add needed error handlers to defer stack. In most real-world cases, we have multiple error checks and only one or just a few error handlers per function. And if whole control flow is thought the ratio is even greater.

Example (CopyFile)
package main

import (
	"fmt"
	"io"
	"os"

	"github.com/lainio/err2"
	"github.com/lainio/err2/try"
)

func main() {
	copyFile := func(src, dst string) (err error) {
		defer err2.Returnf(&err, "copy %s %s", src, dst)

		// These try package helpers are as fast as Check() calls which is as
		// fast as `if err != nil {}`

		r := try.To1(os.Open(src))
		defer r.Close()

		w := try.To1(os.Create(dst))
		defer err2.Handle(&err, func() {
			os.Remove(dst)
		})
		defer w.Close()
		try.To1(io.Copy(w, r))
		return nil
	}

	err := copyFile("/notfound/path/file.go", "/notfound/path/file.bak")
	if err != nil {
		fmt.Println(err)
	}
}
Output:

copy /notfound/path/file.go /notfound/path/file.bak: open /notfound/path/file.go: no such file or directory

Index

Examples

Constants

This section is empty.

Variables

View Source
var Bool _Bool

Bool is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Bools _Bools

Bools is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Byte _Byte

Byte is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Bytes _Bytes

Bytes is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Empty _empty

Empty is deprecated. Use try.To functions instead. Empty is a helper variable to demonstrate how we could build 'type wrappers' to make Try function as fast as Check.

View Source
var File _File

File is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Int _Int

Int is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Ints _Ints

Ints is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var R _R

R is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Request _Request

Request is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Response _Response

Response is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var StrStr _StrStr

StrStr is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var String _String

String is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var Strings _Strings

Strings is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var URL _URL

URL is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

View Source
var W _W

W is a helper variable to generated 'type wrappers' to make Try function as fast as Check.

Functions

func Annotate

func Annotate(prefix string, err *error)

Annotate is for annotating an error. It's similar to Returnf but it takes only two arguments: a prefix string and a pointer to error. It adds ": " between the prefix and the error text automatically.

Example
package main

import (
	"fmt"

	"github.com/lainio/err2"
)

func throw() (string, error) {
	return "", fmt.Errorf("this is an ERROR")
}

func main() {
	annotated := func() (err error) {
		defer err2.Annotate("annotated", &err)
		err2.Try(throw())
		return err
	}
	err := annotated()
	fmt.Printf("%v", err)
}
Output:

annotated: this is an ERROR
Example (DeferStack)
package main

import (
	"fmt"

	"github.com/lainio/err2"
)

func throw() (string, error) {
	return "", fmt.Errorf("this is an ERROR")
}

func main() {
	annotated := func() (err error) {
		defer err2.Annotate("annotated 2nd", &err)
		defer err2.Annotate("annotated 1st", &err)
		err2.Try(throw())
		return err
	}
	err := annotated()
	fmt.Printf("%v", err)
}
Output:

annotated 2nd: annotated 1st: this is an ERROR

func Annotatew added in v0.8.0

func Annotatew(prefix string, err *error)

Annotatew is for annotating an error. It's similar to Returnf but it takes only two arguments: a prefix string and a pointer to error. It adds ": " between the prefix and the error text automatically.

func Catch

func Catch(f func(err error))

Catch is a convenient helper to those functions that doesn't return errors. Go's main function is a good example. Note! There can be only one deferred Catch function per non error returning function. See Handle for more information.

func CatchAll

func CatchAll(errorHandler func(err error), panicHandler func(v any))

CatchAll is a helper function to catch and write handlers for all errors and all panics thrown in the current go routine.

func CatchTrace

func CatchTrace(errorHandler func(err error))

CatchTrace is a helper function to catch and handle all errors. It recovers a panic as well and prints its call stack. This is preferred helper for go workers on long running servers.

func Check

func Check(err error)

Check is deprecated. Use try.To function instead. Check performs error check for the given argument. If the err is nil, it does nothing. According the measurements, it's as fast as

if err != nil {
    return err
}

on happy path.

func FilterTry added in v0.7.0

func FilterTry(filter, err error) bool

FilterTry is deprecated. Use try.Is function instead. FilterTry performs filtered error check for the given argument. It's same as Check but before throwing an error it checks if error matches the filter. The return value false tells that there are no errors and true that filter is matched.

Example
package main

import (
	"bytes"
	"fmt"
	"io"

	"github.com/lainio/err2"
)

func main() {
	copyStream := func(src string) (s string, err error) {
		defer err2.Returnf(&err, "copy stream %s", src)

		in := bytes.NewBufferString(src)
		tmp := make([]byte, 4)
		var out bytes.Buffer
		for n, err := in.Read(tmp); !err2.FilterTry(io.EOF, err); n, err = in.Read(tmp) {
			out.Write(tmp[:n])
		}

		return out.String(), nil
	}

	str, err := copyStream("testing string")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(str)
}
Output:

testing string

func Handle

func Handle(err *error, f func())

Handle is for adding an error handler to a function by defer. It's for functions returning errors them self. For those functions that doesn't return errors there is a Catch function. Note! The handler function f is called only when err != nil.

Example
package main

import (
	"fmt"

	"github.com/lainio/err2"
)

func throw() (string, error) {
	return "", fmt.Errorf("this is an ERROR")
}

func main() {
	doSomething := func(a, b int) (err error) {
		defer err2.Handle(&err, func() {
			err = fmt.Errorf("error with (%d, %d): %v", a, b, err)
		})
		err2.Try(throw())
		return err
	}
	err := doSomething(1, 2)
	fmt.Printf("%v", err)
}
Output:

error with (1, 2): this is an ERROR

func Return

func Return(err *error)

Return is same as Handle but it's for functions which don't wrap or annotate their errors. If you want to annotate errors see Annotate for more information.

Example
package main

import (
	"github.com/lainio/err2"
)

func noThrow() (string, error) { return "test", nil }

func main() {
	var err error
	defer err2.Return(&err)
	err2.Try(noThrow())
}
Output:

func Returnf

func Returnf(err *error, format string, args ...any)

Returnf builds an error. It's similar to fmt.Errorf, but it's called only if error != nil. Note! It doesn't use %w to wrap the error. Use Returnw for that.

Example
package main

import (
	"fmt"

	"github.com/lainio/err2"
)

func throw() (string, error) {
	return "", fmt.Errorf("this is an ERROR")
}

func main() {
	annotated := func() (err error) {
		defer err2.Returnf(&err, "annotated: %s", "err2")
		err2.Try(throw())
		return err
	}
	err := annotated()
	fmt.Printf("%v", err)
}
Output:

annotated: err2: this is an ERROR

func Returnw added in v0.8.0

func Returnw(err *error, format string, args ...any)

Returnw wraps an error. It's similar to fmt.Errorf, but it's called only if error != nil. Note! If you don't want to wrap the error use Returnf instead.

func Try

func Try(args ...any) []any

Try is deprecated. Use try.To functions from try package instead. Try is as similar as proposed Go2 Try macro, but it's a function and it returns slice of interfaces. It has quite big performance penalty when compared to Check function.

func TryEOF added in v0.7.0

func TryEOF(err error) bool

TryEOF is deprecated. Use try.IsEOF function instead. TryEOF checks errors but filters io.EOF from the exception handling and returns boolean which tells if io.EOF is present. See more info from FilterCheck.

Example
package main

import (
	"bytes"
	"fmt"

	"github.com/lainio/err2"
)

func main() {
	copyStream := func(src string) (s string, err error) {
		defer err2.Returnf(&err, "copy stream %s", src)

		in := bytes.NewBufferString(src)
		tmp := make([]byte, 4)
		var out bytes.Buffer
		for n, err := in.Read(tmp); !err2.TryEOF(err); n, err = in.Read(tmp) {
			out.Write(tmp[:n])
		}

		return out.String(), nil
	}

	str, err := copyStream("testing string")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(str)
}
Output:

testing string

Types

This section is empty.

Directories

Path Synopsis
Package assert includes runtime assertion helpers.
Package assert includes runtime assertion helpers.
Package try is a new sub package for `try to` functions which replace all of the error checking.
Package try is a new sub package for `try to` functions which replace all of the error checking.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL