err2

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2020 License: MIT Imports: 4 Imported by: 164

README

err2

The package provides simple helper functions for 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 you fix that.

  1. It helps to declare error handlers with defer.
  2. It helps to check and transport errors to the nearest error handler with panic

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

Error handling

Package err2 relies on the declarative programming to add error handlers. The err2 error handlers are only called if err != nil which makes them convenient to use and reduce boilerplate. To error handlers to work, you must name the error return variable.

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

The Handle is a helper function to add actual error handlers. These handlers 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.

For example, instead of

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

we can call

err2.Try(ioutil.ReadAll(r))

but not without an error handler (Return, Annote, Handle) or it just panics your app if you don't have a recovery call in the goroutines calls stack.

####Type Helpers

The package includes performance optimised versions of Try function where the actual return types of the checked function are told to err2 package by type helper variables. This removes the need to use dynamic type conversion. However, when Go2 generics are out we can replace all of these with generics.

The sample call with type helper variable

data := err2.Bytes.Try(ioutil.ReadAll(r))

The err2 package includes a CLI tool to generate these helpers for your own types. Please see the Makefile for example.

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 package 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 later. There have also been cases when code reports error correctly because the 'upper' handler catches it.
  • Because the use of err2.Annocate is so relatively easy, developers use it always which makes error messages much better and informative. Could say that by this they include a logical call stack in the user-friendly form.
  • There has been a couple of the cases when a quite complex function has needed update. When error handling is based on the actual error handlers, not just passing them up in the stack, the code changes have been much easier. More importantly, because there are no DRY violations fixing is easier.

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 (current)

We will update this package when Go2 generics are released. There is a working version which uses Go generics. We are also monitoring what will happen for the Go-native error handling and tune the library accordingly.

Documentation

Overview

Package err2 provides simple helper functions for error handling.

The traditional error handling idiom in Go is roughly akin to

if err != nil {
	return err
}

which applied recursively. That leads to problems like code noise, redundancy, or even non-checks. The err2 package drives programmers more to focus on error handling rather than checking errors. We think that checks should be as easy as possible that we never forget them.

err2.Try(io.Copy(w, r))

Error checks

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

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

we can write

err2.Try(ioutil.ReadAll(r))

but not without the handler.

Error handling

Package err2 relies on error handlers. In every function which uses err2 for error-checking has to have at least one error handler. If there are no error handlers and error occurs it panics. Panicking for the errors during the development is better than not checking the error at all.

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.

Example (CopyFile)
package main

import (
	"fmt"
	"io"
	"os"

	"github.com/lainio/err2"
)

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

		// These helpers are as fast as Check() calls
		r := err2.File.Try(os.Open(src))
		defer r.Close()

		w := err2.File.Try(os.Create(dst))
		defer err2.Handle(&err, func() {
			os.Remove(dst)
		})
		defer w.Close()
		err2.Empty.Try(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 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 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.

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 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 interface{}))

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 performs the 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 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 ...interface{})

Returnf wraps an error. It's similar to fmt.Errorf, but it's called only if error != nil.

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 Try

func Try(args ...interface{}) []interface{}

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.

Types

This section is empty.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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