err2

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Mar 20, 2024 License: MIT Imports: 8 Imported by: 157

README

test Go Version PkgGoDev Go Report Card

err2

The package extends Go's error handling with fully automatic error checking and propagation like other modern programming languages: Zig, Rust, Swift, etc. err2 isn't an exception handling library, but an entirely orthogonal package with Go's existing error handling mechanism.

func CopyFile(src, dst string) (err error) {
	defer err2.Handle(&err)

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

	w := try.To1(os.Create(dst))
	defer err2.Handle(&err, err2.Err(func(error) {
		try.Out(os.Remove(dst)).Logf("cleaning error")
	}))
	defer w.Close()

	try.To1(io.Copy(w, r))
	return nil
}

go get github.com/lainio/err2

Structure

err2 has the following package structure:

  • The err2 (main) package includes declarative error handling functions.
  • The try package offers error checking functions.
  • The assert package implements assertion helpers for both unit-testing and design-by-contract.

Performance

All of the listed above without any performance penalty! You are welcome to run benchmarks in the project repo and see yourself.

Please note that many benchmarks run 'too fast' according to the common Go benchmarking rules, i.e., compiler optimizations (inlining) are working so well that there are no meaningful results. But for this type of package, where we compete with if-statements, that's precisely what we hope to achieve. The whole package is written toward that goal. Especially with parametric polymorphism, it's been quite the effort.

Automatic Error Propagation

The current version of Go tends to produce too much error checking and too little error handling. But most importantly, it doesn't help developers with automatic error propagation, which would have the same benefits as, e.g., automated garbage collection or automatic testing:

Automation is not just about efficiency but primarily about repeatability and resilience. -- Gregor Hohpe

Automatic error propagation is crucial because it makes your code change tolerant. And, of course, it helps to make your code error-safe:

Never send a human to do a machine's job

The err2 package is your automation buddy:

  1. It helps to declare error handlers with defer. If you're familiar with Zig language, you can think defer err2.Handle(&err,...) line exactly similar as Zig's errdefer.
  2. It helps to check and transport errors to the nearest (the defer-stack) error handler.
  3. It helps us use design-by-contract type preconditions.
  4. It offers automatic stack tracing for every error, runtime error, or panic. If you are familiar with Zig, the err2 error traces are same as Zig's.

You can use all of them or just the other. However, if you use try for error checks, you must remember to use Go's recover() by yourself, or your error isn't transformed to an error return value at any point.

Error handling

The 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.

Every function which uses err2 for error-checking should have at least one error handler. The current function panics if there are no error handlers and an error occurs. However, if any function above in the call stack has an err2 error handler, it will catch the error.

This is the simplest form of err2 automatic error handler:

func doSomething() (err error) {
    // below: if err != nil { return ftm.Errorf("%s: %w", CUR_FUNC_NAME, err) }
    defer err2.Handle(&err)

See more information from err2.Handle's documentation. It supports several error-handling scenarios. And remember that you can have as many error handlers per function as you need, as well as you can chain error handling functions per err2.Handle that allows you to build new error handling middleware for your own purposes.

Error Stack Tracing

The err2 offers optional stack tracing. It's automatic and optimized. Optimized means that the call stack is processed before output. That means that stack trace starts from where the actual error/panic is occurred, not where the error is caught. You don't need to search for the line where the pointer was nil or received an error. That line is in the first one you are seeing:

---
runtime error: index out of range [0] with length 0
---
goroutine 1 [running]:
main.test2({0x0, 0x0, 0x40XXXXXf00?}, 0x2?)
	/home/.../go/src/github.com/lainio/ic/main.go:43 +0x14c
main.main()
	/home/.../go/src/github.com/lainio/ic/main.go:77 +0x248

Just set the err2.SetErrorTracer or err2.SetPanicTracer to the stream you want traces to be written:

err2.SetErrorTracer(os.Stderr) // write error stack trace to stderr
// or, for example:
err2.SetPanicTracer(log.Writer()) // stack panic trace to std logger

If no Tracer is set no stack tracing is done. This is the default because in the most cases proper error messages are enough and panics are handled immediately by a programmer.

Note. Since v0.9.5 you can set these tracers through Go's standard flag package just by adding flag.Parse() to your program. See more information from Automatic Flags.

Read the package documentation for more information.

Error Checks

The try package provides convenient helpers to check the errors. Since the Go 1.18 we have been using generics to have fast and convenient error checking.

For example, instead of

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

we can call

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

but not without an error handler (err2.Handle). 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.

In cases where you want to handle the error immediately after the function call return you can use Go's default if statement. However, we encourage you to use the errdefer concept, defer err2.Handle(&err) for all of your error handling.

Nevertheless, there might be cases where you might want to:

  1. Suppress the error and use some default value.
  2. Just write a logline and continue without a break.
  3. Annotate the specific error value even when you have a general error handler.
  4. You want to handle the specific error value, let's say, at the same line or statement.

The err2/try package offers other helpers based on the DSL concept where the DSL's domain is error-handling. It's based on functions try.Out, try.Out1, and try.Out2, which return instances of types Result, Result1, and Result2. The try.Result is similar to other programming languages, i.e., discriminated union. Please see more from its documentation.

Now we could have the following:

b := try.Out1(strconv.Atoi(s)).Logf("%s => 100", s).Catch(100)

The previous statement tries to convert incoming string value s, but if it doesn't succeed, it writes a warning to logs and uses the default value (100). The logging result includes the original error message as well.

It's easy to see that panicking about the errors at the start of the development is far better than not checking errors at all. But most importantly, err2/try keeps the code readable.

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 try.Is and even try.IsEOF for convenience.

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

notExist := try.Is(r2.err, plugin.ErrNotExist)

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

Note. Any other error than plugin.ErrNotExist is treated as an real error:

  1. try.Is function first checks if err == nil, and if yes, it returns false.
  2. Then it checks if errors.Is(err, plugin.ErrNotExist) and if yes, it returns true.
  3. Finally, it calls try.To for the non nil error, and we already know what then happens: nearest err2.Handle gets it first.

These try.Is functions help cleanup mesh idiomatic Go, i.e. mixing happy and error path, leads to.

For more information see the examples in the documentation of both functions.

Backwards Compatibility Promise for the API

The err2 package's API will be backward compatible. Before version 1.0.0 is released, the API changes occasionally, but we promise to offer automatic conversion scripts for your repos to update them for the latest API. We also mark functions deprecated before they become obsolete. Usually, one released version before. We have tested this with a large code base in our systems, and it works wonderfully.

More information can be found in the scripts/ directory readme file.

Assertion

The assert package is meant to be used for design-by-contract- type of development where you set pre- and post-conditions for your functions. It's not meant to replace the normal error checking but speed up the incremental hacking cycle. The default mode is to return an error value that includes a formatted and detailed assertion violation message. A developer gets immediate and proper feedback, allowing cleanup of the code and APIs before the release.

Asserters

The assert package offers a few pre-build asserters, which are used to configure how the assert package deals with assert violations. The line below exemplifies how the default asserter is set in the package.

assert.SetDefault(assert.Production)

If you want to suppress the caller info (source file name, line number, etc.) and get just the plain panics from the asserts, you should set the default asserter with the following line:

assert.SetDefault(assert.Debug)

For certain type of programs this is the best way. It allows us to keep all the error messages as simple as possible. And by offering option to turn additional information on, which allows super users and developers get more technical information when needed.

Note. Since v0.9.5 you can set these asserters through Go's standard flag package just by adding flag.Parse() to your program. See more information from Automatic Flags.

Assertion Package for Runtime Use

Following is example of use of the assert package:

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

We have now described design-by-contract for development and runtime use. What makes err2's assertion packages unique, and extremely powerful, is its use for automatic testing as well.

Assertion Package for Unit Testing

The same asserts can be used and shared during the unit tests:

func TestWebOfTrustInfo(t *testing.T) {
	defer assert.PushTester(t)()

	common := dave.CommonChains(eve.Node)
	assert.SLen(common, 2)

	wot := dave.WebOfTrustInfo(eve.Node) //<- this includes asserts as well!!
	// And if there's violations during the test run they are reported as
	// test failures for this TestWebOfTrustInfo -test.

	assert.Equal(0, wot.CommonInvider)
	assert.Equal(1, wot.Hops)

	wot = NewWebOfTrust(bob.Node, carol.Node)
	assert.Equal(-1, wot.CommonInvider)
	assert.Equal(-1, wot.Hops)
	...

A compelling feature is that even if some assertion violation happens during the execution of called functions like the above NewWebOfTrust() function instead of the actual Test function, it's reported as a standard test failure. That means we don't need to open our internal pre- and post-conditions just for testing.

We can share the same assertions between runtime and test execution.

The err2 assert package integration to the Go testing package is completed at the cross-module level. Suppose package A uses package B. If package B includes runtime asserts in any function that A calls during testing and some of B's asserts fail, A's current test also fails. There is no loss of information, and even the stack trace is parsed to test logs for easy traversal. Packages A and B can be the same or different modules.

This means that where ever assertion violation happens during the test execution, we will find it and can even move thru every step in the call stack.

Automatic Flags

When you are using err2 or assert packages, i.e., just importing them, you have an option to automatically support for err2 configuration flags through Go's standard flag package. See more information about err2 settings from Error Stack Tracing and Asserters.

Now you can always deploy your applications and services with the simple end-user friendly error messages and no stack traces, but you can switch them on when ever you need.

Let's say you have build CLI (your-app) tool with the support for Go's flag package, and the app returns an error. Let's assume you're a developer. You can run it again with:

your-app -err2-trace stderr

Now you get full error trace addition to the error message. Naturally, this also works with assertions. You can configure their output with the flag asserter:

your-app -asserter Debug

That adds more information to the assertion statement, which in default is in production (Prod) mode, i.e., outputs a single-line assertion message.

All you need to do is to add flag.Parse to your main function.

Support for Cobra Flags

If you are using cobra you can still easily support packages like err2 and glog and their flags.

  1. Add std flag package to imports in cmd/root.go:

    import (
        goflag "flag"
        ...
    )
    
  2. Add the following to (usually) cmd/root.go's init function's end:

    func init() {
        ...
        // NOTE! Very important. Adds support for std flag pkg users: glog, err2
        pflag.CommandLine.AddGoFlagSet(goflag.CommandLine)
    }
    
  3. And finally modify your PersistentPreRunE in cmd/root.go to something like:

    PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
        defer err2.Handle(&err)
    
        // NOTE! Very important. Adds support for std flag pkg users: glog, err2
        goflag.Parse()
    
        try.To(goflag.Set("logtostderr", "true"))
        handleViperFlags(cmd) // local helper with envs
        glog.CopyStandardLogTo("ERROR") // for err2
        return nil
    },
    

As a result you can have bunch of usable flags added to your CLI:

Flags:
      --asserter asserter                 asserter: Plain, Prod, Dev, Debug (default Prod)
      --err2-log stream                   stream for logging: nil -> log pkg (default nil)
      --err2-panic-trace stream           stream for panic tracing (default stderr)
      --err2-trace stream                 stream for error tracing: stderr, stdout (default nil)
      ...

Code Snippets

Most of the repetitive code blocks are offered as code snippets. They are in ./snippets in VC code format, which is well supported e.g. neovim, etc.

The snippets must be installed manually to your preferred IDE/editor. During the installation you can modify the according your style or add new ones. We would prefer if you could contribute some of the back to the err2 package.

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 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. But be aware that something is wrong if your code uses the error path as part of the algorithm itself.

For happy paths by using try.To* or assert.That error check functions there are no performance penalty at all. However, the mandatory use of the defer might prevent some code optimisations like function inlining. And still, we have cases where using the err2 and try package simplify the algorithm so that it's faster than the return value if err != nil version. (See the benchmarks for io.Copy in the repo.)

If you have a performance-critical use case, we always recommend you to write performance tests to measure the effect. As a general guideline for maximum performance we recommend to put error handlers as high in the call stack as possible, and use only error checking (try.To() calls) in the inner loops. And yes, that leads to non-local control structures, but it's the most performant solution of all. (The repo has benchmarks for that as well.)

The original goal was to make it possible to write similar code that the proposed Go2 error handling would allow and do it right now (summer 2019). The goal was well aligned with the Go2 proposal, where it would bring a try macro and let the error handling be implemented in defer blocks. The try-proposal was canceled at its latest form. Nevertheless, we have learned that using panics for early-stage error transport isn't bad but the opposite. It seems to help:

  • to draft algorithms much faster,
  • huge improvements for the readability,
  • helps to bring a new blood (developers with different programming language background) to projects,
  • and most importantly, it keeps your code more refactorable because you don't have to repeat yourself.

Learnings by so far

We have used the err2 and assert packages in several 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 (and optionally stack traces) 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.Handle is so easy, error messages are much better and informative. When using err2.Handle's automatic annotation your error messages are always up-to-date. Even when you refactor your function name error message is also updated.

  • When error handling is based on the actual error handlers, code changes have been much easier. There is an excellent blog post about the issues you are facing with Go's error handling without the help of the err2 package.

Support And Contributions

The package has been in experimental mode quite long time. Since the Go generics we are transiting towards more official mode. Currently we offer support by GitHub Discussions. Naturally, any issues and contributions are welcome as well!

Roadmap

Please see the full version history from CHANGELOG.

Latest Release
1.0.0
  • Finally! We are very happy, and thanks to all who have helped!
  • Lots of documentation updates and cleanups for version 1.0.0
  • Catch/Handle take unlimited amount error handler functions
    • allows building e.g. error handling middlewares
    • this is major feature because it allows building helpers/add-ons
  • automatic outputs aren't overwritten by given args, only with assert.Plain
  • Minor API fixes to still simplify it:
    • remove exported vars, obsolete types and funcs from assert pkg
    • Result2.Def2() sets only Val2
  • technical refactorings: variadic function calls only in API level

Documentation

Overview

Package err2 provides three main functionality:

  1. err2 package includes helper functions for error handling & automatic error stack tracing
  2. github.com/lainio/err2/try sub-package is for error checking
  3. github.com/lainio/err2/assert sub-package is for design-by-contract and preconditions both for normal runtime and for unit testing

The err2 package drives programmers to focus on error handling rather than checking errors. We think that checks should be so easy that we never forget them. The CopyFile example shows how it works:

// CopyFile copies source file to the given destination. If any error occurs it
// returns error value describing the reason.
func CopyFile(src, dst string) (err error) {
     // Add first error handler is to catch and annotate the error properly.
     defer err2.Handle(&err)

     // Try to open the file. If error occurs now, err will be
     // automatically annotated ('copy file:' prefix calculated from the
     // function name, no performance penalty) and returned properly thanks
     // to above err2.Handle.
     r := try.To1(os.Open(src))
     defer r.Close()

     // Try to create a file. If error occurs now, err will be annotated and
     // returned properly.
     w := try.To1(os.Create(dst))
     // Add error handler to clean up the destination file in case of
     // error. Handler fn is called only if there has been an error at the
     // following try.To check. We place it here that the next deferred
     // close is called before our Remove a file call.
     defer err2.Handle(&err, err2.Err(func(error) {
     	try.Out(os.Remove(dst)).Logf("cleanup failed")
     }))
     defer w.Close()

     // Try to copy the file. If error occurs now, all previous error handlers
     // will be called in the reversed order. And a final error value is
     // properly annotated and returned in all the cases.
     try.To1(io.Copy(w, r))

     // All OK, just return nil.
     return nil
}

Error checks and Automatic Error Propagation

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

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

we can write

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

Note that try.To functions are as fast as if err != nil statements. Please see the github.com/lainio/err2/try package documentation for more information about the error checks.

Automatic Stack Tracing

err2 offers optional stack tracing. And yes, it's fully automatic. Just call

flag.Parse() # this is enough for err2 pkg to add its flags

at the beginning your app, e.g. main function, or set the tracers programmatically (before flag.Parse if you are using that):

err2.SetErrorTracer(os.Stderr) // write error stack trace to stderr
 or
err2.SetPanicTracer(log.Writer()) // panic stack trace to std logger

Note. Since Catch's default mode is to recover from panics, it's a good practice still print their stack trace. The panic tracer's default values is os.Stderr. The default error tracer is nil.

err2.SetPanicTracer(os.Stderr) // panic stack tracer's default is stderr
err2.SetErrorTracer(nil) // error stack tracer's default is nil

Note that both panic and error traces are optimized by err2 package. That means that the head of the stack trace isn't the panic function, but an actual line that caused it. It works for all three categories:

  • normal error values
  • runtime.Error values
  • any types of the panics

The last two types are handled as panics in the error handling functions given to Handle and Catch.

Automatic Logging

Same err2 capablities support automatic logging like the Catch and [try.Result.Logf] functions. To be able to tune up how logging behaves we offer a tracer API:

err2.SetLogTracer(nil) // the default is nil where std log pkg is used.

Flag Package Support

The err2 package supports Go's flags. All you need to do is to call flag.Parse. And the following flags are supported (="default-value"):

-err2-log="nil"
    A name of the stream currently supported stderr, stdout or nil
-err2-panic-trace="stderr"
    A name of the stream currently supported stderr, stdout or nil
-err2-trace="nil"
    A name of the stream currently supported stderr, stdout or nil

Note that you have called SetErrorTracer and others, before you call flag.Parse. This allows you set the defaults according your app's need and allow end-user change them during the runtime.

Error handling

Package err2 relies on declarative control structures to achieve error and panic safety. In every function which uses err2 or try package for error-checking has to have at least one declarative error handler if it returns error value. If there are no error handlers and error occurs it panics. We think that panicking for the errors is much better than not checking errors at all. Nevertheless, if the call stack includes any err2 error handlers like Handle the error is handled where the handler is saved to defer-stack. (defer is not lexically scoped)

err2 includes many examples to play with like previous CopyFile. Please see them for more information.

Example
//go:build !windows

package main

import (
	"fmt"
	"io"
	"os"

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

func CopyFile(src, dst string) (err error) {
	// Automatic error annotation from current function name.
	defer err2.Handle(&err)

	// NOTE. These try.To() checkers are as fast as `if err != nil {}`
	r := try.To1(os.Open(src))
	defer r.Close() // deferred resource cleanup is perfect match with err2

	w := try.To1(os.Create(dst))
	defer err2.Handle(&err, func() {
		// If error happens during Copy we clean not completed file here
		// Look how well it suits with other cleanups like Close calls.
		os.Remove(dst)
	})
	defer w.Close()
	try.To1(io.Copy(w, r))
	return nil
}

func main() {
	// To see how automatic stack tracing works please run this example with:
	//   go test -v -run='^Example$'
	err2.SetErrorTracer(os.Stderr)

	err := CopyFile("/notfound/path/file.go", "/notfound/path/file.bak")
	if err != nil {
		fmt.Println(err)
	}
	// in real word example 'run example' is 'copy file' it comes automatically
	// from function name that calls `err2.Handle` in deferred.

}
Output:

testing: run example: open /notfound/path/file.go: no such file or directory

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNotFound       = errors.New("not found")
	ErrNotExist       = errors.New("not exist")
	ErrAlreadyExist   = errors.New("already exist")
	ErrNotAccess      = errors.New("permission denied")
	ErrNotEnabled     = errors.New("not enabled")
	ErrNotRecoverable = errors.New("cannot recover")
	ErrRecoverable    = errors.New("recoverable")
)

Sentinel error value helpers. They are convenient thanks to github.com/lainio/err2/try.IsNotFound and similar functions.

ErrNotFound ... ErrNotEnabled are similar no-error like io.EOF for those who really want to use error return values to transport non errors. It's far better to have discriminated unions as errors for function calls. But if you insist the related helpers are in they github.com/lainio/err2/try package: github.com/lainio/err2/try.IsNotFound, ...

ErrRecoverable and ErrNotRecoverable since Go 1.20 wraps multiple errors same time, i.e. wrapped errors aren't list anymore but tree. This allows mark multiple semantics to same error. These error are mainly for that purpose.

View Source
var Stdnull = &nullDev{}

Stdnull implements io.Writer that writes nothing, e.g., SetLogTracer in cases you don't want to use automatic log writer (=nil), i.e., LogTracer == /dev/null. It can be used to change how the Catch works, e.g., in CLI apps.

Functions

func Catch

func Catch(a ...any)

Catch is a convenient helper to those functions that doesn't return errors. Note that Catch always catch the panics. If you don't want to stop them (i.e., use of [recover]) you should add panic handler and continue panicking there. There can be only one deferred Catch function per non error returning functions, i.e. goroutine functions like main(). There is several ways to use the Catch function. And always remember the [defer].

The deferred Catch is very convenient, because it makes your current goroutine panic and error-safe. You can fine tune its 'global' behavior with functions like SetErrorTracer, SetPanicTracer, and SetLogTracer. Its 'local' behavior depends the arguments you give it. Let's start with the defaults and simplest version of Catch:

defer err2.Catch()

In default the above writes errors to logs and panic traces to stderr. Naturally, you can annotate logging:

defer err2.Catch("WARNING: caught errors: %s", name)

The preceding line catches the errors and panics and prints an annotated error message about the error source (from where the error was thrown) to the currently set log. Note, when log stream isn't set, the standard log is used. It can be bound to, e.g., glog. And if you want to suppress automatic logging entirely use the following setup:

err2.SetLogTracer(err2.Stdnull)

The next one stops errors and panics, but allows you handle errors, like cleanups, etc. The error handler function has same signature as Handle's error handling function Handler. By returning nil resets the error, which allows e.g. prevent automatic error logs to happening. Otherwise, the output results depends on the current trace and assert settings. The default trace setting prints call stacks for panics but not for errors:

defer err2.Catch(func(err error) error { return err} )

or if you you prefer to use dedicated helpers:

defer err2.Catch(err2.Noop)

You can give unlimited amount of error handlers. They are called if error happens and they are called in the same order as they are given or until one of them resets the error like Reset in the next sample:

defer err2.Catch(err2.Noop, err2.Reset, err2.Log) // err2.Log not called!

The next sample calls your error handler, and you have an explicit panic handler as well, where you can e.g. continue panicking to propagate it for above callers or stop it like below:

defer err2.Catch(func(err error) error { return err }, func(p any) {})
Example (WithFmt)
package main

import (
	"os"

	"github.com/lainio/err2"
)

func main() {
	// Set default logger to stdout for this example
	oldLogW := err2.LogTracer()
	err2.SetLogTracer(os.Stdout)
	defer err2.SetLogTracer(oldLogW)

	transport := func() {
		// See how Catch follows given format string similarly as Handle
		defer err2.Catch("catch")
		err2.Throwf("our error")
	}
	transport()
}
Output:

catch: our error

func ErrorTracer added in v0.8.9

func ErrorTracer() io.Writer

ErrorTracer returns current io.Writer for automatic error stack tracing. The default value is nil.

func Formatter added in v0.8.13

func Formatter() formatter.Interface

Returns the current formatter. See more information from SetFormatter and formatter package.

func Handle

func Handle(err *error, a ...any)

Handle is the general purpose error handling function. What makes it so convenient is its ability to handle all error handling cases:

  • just return the error value to caller
  • annotate the error value
  • execute real error handling like cleanup and releasing resources.

There's no performance penalty. The handler is called only when err != nil. There's no limit how many Handle functions can be added to defer stack. They all are called if an error has occurred.

The function has an automatic mode where errors are annotated by function name if no annotation arguments or handler function is given:

func SaveData(...) (err error) {
     defer err2.Handle(&err) // if err != nil: annotation is "save data:"

Note. If you are still using sentinel errors you must be careful with the automatic error annotation because it uses wrapping. If you must keep the error value got from error checks: github.com/lainio/err2/try.To, you must disable automatic error annotation (%w), or set the returned error values in the handler function. Disabling can be done by setting second argument nil:

func SaveData(...) (err error) {
     defer err2.Handle(&err, nil) // nil arg disable automatic annotation.

In case of the actual error handling, the handler function should be given as a second argument:

defer err2.Handle(&err, func(err error) error {
     if rmErr := os.Remove(dst); rmErr != nil {
          return fmt.Errorf("%w: cleanup error: %w", err, rmErr)
     }
     return err
})

You can have unlimited amount of error handlers. They are called if error happens and they are called in the same order as they are given or until one of them resets the error like Reset (notice the other predefined error handlers) in the next samples:

defer err2.Handle(&err, err2.Reset, err2.Log) // Log not called
defer err2.Handle(&err, err2.Noop, err2.Log) // handlers > 1: err annotated
defer err2.Handle(&err, nil, err2.Log) // nil disables auto-annotation

If you need to stop general panics in a handler, you can do that by declaring a panic handler. See the second handler below:

defer err2.Handle(&err,
     err2.Err( func(error) { os.Remove(dst) }), // err2.Err() keeps it short
     // below handler catches panics, but you can re-throw if needed
     func(p any) {}
)
Example
package main

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

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

func main() {
	var err error
	defer err2.Handle(&err)
	try.To1(noThrow())
}
Output:

Example (Annotate)
package main

import (
	"fmt"

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

const errStringInThrow = "this is an ERROR"

func throw() (string, error) {
	return "", fmt.Errorf(errStringInThrow)
}

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

annotated: err2: this is an ERROR
Example (AnnotatedErrReturn)
package main

import (
	"fmt"

	"github.com/lainio/err2"
)

func main() {
	normalReturn := func() (err error) {
		defer err2.Handle(&err) // automatic annotation
		return fmt.Errorf("our error")
	}
	err := normalReturn()
	fmt.Printf("%v", err)

	// ------- func name comes from Go example/test harness
	// ------- v ------------------ v --------
	
Output:

Example (DeferStack)
package main

import (
	"fmt"

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

const errStringInThrow = "this is an ERROR"

func throw() (string, error) {
	return "", fmt.Errorf(errStringInThrow)
}

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

annotated 2nd: annotated 1st: this is an ERROR
Example (Empty)
package main

import (
	"fmt"

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

const errStringInThrow = "this is an ERROR"

func throw() (string, error) {
	return "", fmt.Errorf(errStringInThrow)
}

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

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

import (
	"fmt"

	"github.com/lainio/err2"
)

func main() {
	normalReturn := func() (err error) {
		defer err2.Handle(&err, nil) // nil disables automatic annotation
		return fmt.Errorf("our error")
	}
	err := normalReturn()
	fmt.Printf("%v", err)
}
Output:

our error
Example (ErrThrow)
package main

import (
	"fmt"

	"github.com/lainio/err2"
)

func main() {
	transport := func() (err error) {
		defer err2.Handle(&err)
		err2.Throwf("our error")
		return nil
	}
	err := transport()
	fmt.Printf("%v", err)
}
Output:

testing: run example: our error
Example (HandlerFn)
package main

import (
	"fmt"

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

const errStringInThrow = "this is an ERROR"

func throw() (string, error) {
	return "", fmt.Errorf(errStringInThrow)
}

func main() {
	doSomething := func(a, b int) (err error) {
		defer err2.Handle(&err, func(err error) error {
			// Example for just annotating current err. Normally Handle is
			// used for e.g. cleanup, not annotation that can be left for
			// err2 automatic annotation. See CopyFile example for more
			// information.
			return fmt.Errorf("error with (%d, %d): %v", a, b, err)
		})
		try.To1(throw())
		return err
	}
	err := doSomething(1, 2)
	fmt.Printf("%v", err)
}
Output:

error with (1, 2): this is an ERROR
Example (MultipleHandlerFns)
package main

import (
	"fmt"

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

const errStringInThrow = "this is an ERROR"

func throw() (string, error) {
	return "", fmt.Errorf(errStringInThrow)
}

func main() {
	doSomething := func(a, b int) (err error) {
		defer err2.Handle(&err,
			// cause automatic annotation <== 2 error handlers do the trick
			err2.Noop,
			func(err error) error {
				// Example for just annotating current err. Normally Handle
				// is used for e.g. cleanup, not annotation that can be left
				// for err2 automatic annotation. See CopyFile example for
				// more information.
				return fmt.Errorf("%w error with (%d, %d)", err, a, b)
			})
		try.To1(throw())
		return err
	}
	err := doSomething(1, 2)
	fmt.Printf("%v", err)
}
Output:

testing: run example: this is an ERROR error with (1, 2)
Example (NoThrow)
package main

import (
	"fmt"

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

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

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

<nil>

func Log added in v1.0.0

func Log(err error) error

Log is a built-in helper to use with Handle and Catch. Log prints error string to the current log that is set by SetLogTracer.

func LogTracer added in v0.9.5

func LogTracer() io.Writer

LogTracer returns a current io.Writer for the explicit [try.Result.Logf] function and automatic logging used in Handle and Catch. The default value is nil.

func Noop added in v0.9.5

func Noop(err error) error

Noop is a built-in helper to use with Handle and Catch. It keeps the current error value the same. You can use it like this:

defer err2.Handle(&err, err2.Noop)

func PanicTracer added in v0.8.9

func PanicTracer() io.Writer

PanicTracer returns current io.Writer for automatic panic stack tracing. Note that runtime.Error types which are transported by panics are controlled by this. The default value is os.Stderr.

func Reset added in v0.9.5

func Reset(error) error

Reset is a built-in helper to use with Handle and Catch. It sets the current error value to nil. You can use it like this to reset the error:

defer err2.Handle(&err, err2.Reset)

func SetErrorTracer added in v0.8.9

func SetErrorTracer(w io.Writer)

SetErrorTracer sets a io.Writer for automatic error stack tracing. The err2 default is nil. Note that any function that has deferred Handle or Catch is capable to print error stack trace:

func CopyFile(src, dst string) (err error) {
     defer err2.Handle(&err) // <- makes error trace printing decision

Error trace is almost the same format as Go's standard call stack but it may have multiple sections because every Handle and Catch prints it. If an error happens in a deep call stack, the error trace includes various parts. The principle is similar to Zig Error Return Traces, where you see how error bubbles up. However, our error trace is a combination of error return traces and stack traces because we get all the needed information at once.

Remember that you can reset these with flag package support. See documentation of err2 package's flag section.

func SetFormatter added in v0.8.13

func SetFormatter(f formatter.Interface)

SetFormatter sets the current formatter for the err2 package. The default formatter.Decamel processes function names to human readable and the idiomatic Go format, i.e. all lowercase, space delimiter, package names colon separated. The example how a quite complex method name gives a proper error message prefix:

"ssi.(*DIDAgent).CreateWallet" -> "ssi: didagent create wallet"

Following line sets a noop formatter where errors are taken as function names are in the call stack.

err2.SetFormatter(formatter.Noop)

You can make your own implementations of formatters. See more information in formatter package.

func SetLogTracer added in v0.9.5

func SetLogTracer(w io.Writer)

SetLogTracer sets a current io.Writer for the explicit [try.Result.Logf] function and automatic logging used in Handle and Catch. The default is nil and then err2 uses std log package for logging.

You can use the std log package to redirect other logging packages like [glog] to automatically work with the err2 package. For the [glog], add this line at the beginning of your app:

glog.CopyStandardLogTo("INFO")

Remember that you can reset these with flag package support. See documentation of err2 package's flag section.

func SetPanicTracer added in v0.8.9

func SetPanicTracer(w io.Writer)

SetPanicTracer sets a io.Writer for automatic panic stack tracing. The err2 default is os.Stderr. Note that runtime.Error types which are transported by panics are controlled by this. Note also that the current function is capable to print panic stack trace when the function has at least one deferred error handler, e.g:

func CopyFile(src, dst string) (err error) {
     defer err2.Handle(&err) // <- panic trace print decision is done here

Remember that you can reset these with flag package support. See documentation of err2 package's flag section.

func SetTracers added in v0.8.9

func SetTracers(w io.Writer)

SetTracers a helper to set a io.Writer for error and panic stack tracing, the log tracer is set as well. More information see SetErrorTracer, SetPanicTracer, and SetLogTracer functions.

Remember that you can reset these with flag package support. See documentation of err2 package's flag section.

func Stderr added in v0.9.52

func Stderr(err error) error

Stderr is a built-in helper to use with Handle and Catch. It prints the error to stderr and it resets the current error value. It's a handy Catch handler in main function.

You can use it like this:

func main() {
     defer err2.Catch(err2.Stderr)

func StderrNoReset added in v1.0.0

func StderrNoReset(err error) error

StderrNoReset is a built-in helper to use with Handle and Catch. It prints the error to stderr. If you need to reset err value use Stderr instead.

You can use it like this:

func myFunction() {
     defer err2.Handle(err2.Noop, err2.StderrNoReset)

func Stdout added in v0.9.52

func Stdout(err error) error

Stdout is a built-in helper to use with Handle and Catch. It prints the error to stdout and it resets the current error value. It's a handy Catch handler in main function.

You can use it like this:

func main() {
     defer err2.Catch(err2.Stdout)

func StdoutNoReset added in v1.0.0

func StdoutNoReset(err error) error

StdoutNoReset is a built-in helper to use with Handle and Catch. It prints the error to stdout.

You can use it like this:

func main() {
     defer err2.Catch(err2.StdoutNoReset)

func Throwf added in v0.8.7

func Throwf(format string, args ...any)

Throwf builds and throws an error (panic). For creation it's similar to fmt.Errorf. Because panic is used to transport the error instead of error return value, it's called only if you want to non-local control structure for error handling, i.e. your current function doesn't have error return value.

  • Throwf is rarely needed. We suggest to use error return values instead.

Throwf is offered for deep recursive algorithms to help readability and performance (see bechmarks) in those cases.

func yourFn() (res any) {
     ...
     if badHappens {
          err2.Throwf("we cannot do that for %v", subject)
     }
     ...
}
Example
package main

import (
	"fmt"

	"github.com/lainio/err2"
)

func main() {
	type fn func(v int) int
	var recursion fn
	const recursionLimit = 77 // 12+11+10+9+8+7+6+5+4+3+2+1 = 78

	recursion = func(i int) int {
		if i > recursionLimit { // simulated error case
			err2.Throwf("helper failed at: %d", i)
		} else if i == 0 {
			return 0 // recursion without error ends here
		}
		return i + recursion(i-1)
	}

	annotated := func() (err error) {
		defer err2.Handle(&err, "annotated: %s", "err2")

		r := recursion(12) // call recursive algorithm successfully
		recursion(r)       // call recursive algorithm unsuccessfully
		return err
	}
	err := annotated()
	fmt.Printf("%v", err)
}
Output:

annotated: err2: helper failed at: 78

Types

type Handler added in v0.9.5

type Handler = handler.ErrorFn

Handler is a function type used to process error values in Handle and Catch. We currently have a few build-ins of the Handler: Noop, Reset, etc.

func Err added in v0.9.5

func Err(f func(err error)) Handler

Err is a built-in helper to use with Handle and Catch. It offers simplifier for error handling function for cases where you don't need to change the current error value. For instance, if you want to just write error to stdout, and don't want to use SetLogTracer and keep it to write to your logs.

defer err2.Catch(err2.Err(func(err error) {
     fmt.Println("ERROR:", err)
}))

Note that since Err helper we have other helpers like Stdout that allows previous block be written as simple as:

defer err2.Catch(err2.Stdout)

Directories

Path Synopsis
Package assert includes runtime assertion helpers both for normal execution as well as a assertion package for Go's testing.
Package assert includes runtime assertion helpers both for normal execution as well as a assertion package for Go's testing.
Package formatter implements formatters and helper types for err2.
Package formatter implements formatters and helper types for err2.
internal
formatter
Package formatter imlements thread safe storage for Formatter interface.
Package formatter imlements thread safe storage for Formatter interface.
handler
Package handler implements handler for objects returned recovery() function.
Package handler implements handler for objects returned recovery() function.
str
tracer
Package tracer implements thread safe storage for trace writers.
Package tracer implements thread safe storage for trace writers.
x
Package main includes samples of err2.
Package main includes samples of err2.
Package try is a package for [To], [To1], and [To2] functions that implement the error checking.
Package try is a package for [To], [To1], and [To2] functions that implement the error checking.

Jump to

Keyboard shortcuts

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