Documentation ¶
Overview ¶
Package err2 provides three main functionality:
- err2 package includes helper functions for error handling & automatic error stack tracing
- try package is for error checking
- assert package is for design-by-contract and preconditions both for normal runtime and for testing
The traditional error handling idiom in Go is roughly akin to
if err != nil { return err }
which applied recursively.
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 just to 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. Place it here that // the next deferred close is called before our Remove call. defer err2.Handle(&err, err2.Err(func(error) { os.Remove(dst) })) defer w.Close() // Try to copy the file. If error occurs now, all previous error handlers // will be called in the reversed order. And final return error is // properly annotated in all the cases. try.To1(io.Copy(w, r)) // All OK, just return nil. return nil }
Error checks and Automatic Error Propagation ¶
The try package 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))
Note that try.ToX functions are as fast as if err != nil statements. Please see the 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 set the tracers at the beginning your app, e.g. main function, to the stream you want traces to be written:
err2.SetErrorTracer(os.Stderr) // write error stack trace to stderr or err2.SetPanicTracer(log.Writer()) // panic stack trace to std logger
Note. Since err2.Catch's default mode is to catch panics, 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
Automatic Logging ¶
Same err2 capablities support automatic logging like the err2.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
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 err2.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 ¶
- Variables
- func Catch(a ...any)
- func Err(f func(err error)) func(error) error
- func ErrorTracer() io.Writer
- func Formatter() formatter.Interface
- func Handle(err *error, a ...any)
- func LogTracer() io.Writer
- func Noop(err error) error
- func PanicTracer() io.Writer
- func Reset(error) error
- func SetErrorTracer(w io.Writer)
- func SetFormatter(f formatter.Interface)
- func SetLogTracer(w io.Writer)
- func SetPanicTracer(w io.Writer)
- func SetTracers(w io.Writer)
- func Throwf(format string, args ...any)
- type Handler
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotFound is 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 try package: try.IsNotFound(), ... These // 'global' errors and their helper functions in try package are for // experimenting now. 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") // 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. ErrNotRecoverable = errors.New("cannot recover") ErrRecoverable = errors.New("recoverable") )
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 (recover) you should add panic handler and continue panicking there. There can be only one deferred Catch function per non error returning function 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, one line only! You can fine tune its behavior with functions like err2.SetErrorTrace, assert.SetDefault, and logging settings. 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.
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, i.e., err2.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 Tracer and assert settings. Default setting print 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)
The last one calls your error handler, and you have an explicit panic handler too, 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 Err ¶ added in v0.9.5
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) }))
func ErrorTracer ¶ added in v0.8.9
ErrorTracer returns current io.Writer for automatic error stack tracing. The default value is nil.
func Formatter ¶ added in v0.8.13
Returns the current formatter. See more information from SetFormatter and formatter package.
func Handle ¶
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 is no performance penalty. The handler is called only when err != nil. There is 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: '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 an 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 })
If you need to stop general panics in handler, you can do that by giving a panic handler function:
defer err2.Handle(&err, err2.Err( func(error) { os.Remove(dst) }), // err2.Err() keeps it short func(p any) {} // <- handler stops panics, re-throw or not )
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 (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, "") 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 cleanup. 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 (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 LogTracer ¶ added in v0.9.5
LogTracer returns current io.Writer for try.Out().Logf(). The default value is nil.
func Noop ¶ added in v0.9.5
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
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
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
SetErrorTracer sets a io.Writer for automatic error stack tracing. The err2 default is nil. Note that the current function is capable to print error 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) // <- error trace print decision is done here
func SetFormatter ¶ added in v0.8.13
SetFormatter sets the current formatter for the err2 package. The default formatter.Decamel tries to process function names to human readable and the idiomatic Go format, i.e. all lowercase, space delimiter, etc.
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
SetLogTracer sets a io.Writer for try.Out().Logf() function. The default is nil and then err2 uses std log package for logging.
You can use that to redirect packages like glog and have proper logging. For glog, add this line at the beginning of your app:
glog.CopyStandardLogTo("INFO")
func SetPanicTracer ¶ added in v0.8.9
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) // <- error trace print decision is done here
func SetTracers ¶ added in v0.8.9
SetTracers a convenient helper to set a io.Writer for error and panic stack tracing. More information see SetErrorTracer and SetPanicTracer functions.
func Throwf ¶ added in v0.8.7
Throwf builds and throws (panics) an error. 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 ¶
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. |
tracer
Package tracer implements thread safe storage for trace writers.
|
Package tracer implements thread safe storage for trace writers. |
Package main includes samples of err2.
|
Package main includes samples of err2. |
Package try is a package for try.ToX functions that implement the error checking.
|
Package try is a package for try.ToX functions that implement the error checking. |