Skip to content

mrsoftware/errors

Repository files navigation

errors

Enriching Error Handling in Go

The errors package aims to empower you with a more robust approach to error handling in your Go applications. It addresses the limitations of Go's default error handling by providing features for:

  • Contextual Information: Add meaningful details to errors, making them more informative for debugging and troubleshooting.
  • Error Chaining: Create a chain of errors, capturing the root cause and subsequent events leading to the final error.
  • Multiple Data: Attach various types of data (strings, structs, etc.) to errors for richer context.
  • MultiError: Group and manage multiple errors without losing their individual structure. It's concurrent safe, can be used in multiple goroutine.
  • WaitGroup: Streamline m error handling by potentially providing abstractions for common use cases like MultiError + sync.WaitGroup.

Installation

go get github.com/mrsoftware/errors

Beyond Basic Error Handling

Go's traditional error handling often results in error messages lacking context and debugging information, especially when errors propagate up the call stack. The errors package tackles this challenge by allowing you to enrich error messages with valuable data.

Adding Context

The errors.New function is your go-to tool for creating errors with context. You can pass additional arguments to specify different types of data:

user := struct { Username string }{Username: "mrsoftware"}
errors.New("some error", errors.String("name", "mohammad"), errors.Reflect("user", user))

Retrieving Context

To extract the added data from an error:

  • Single Error:
err := errors.New("some error", errors.String("username", "mrsoftware"))
nameField := errors.GetField(err, "username")
  • Error Chain:
cause := errors.New("cause error", errors.String("username", "mrsoftware"))
err := errors.Wrap(err, "some error", errors.String("name", "mohammad"))
nameField := errors.FindFieldInChain(err, "username")

Wrapping Existing Errors

The errors.Wrap function lets you add context to existing errors while preserving the original cause:

_, err := ioutil.ReadAll(r)
if err != nil {
        errors.Wrap(err, "some error", errors.String("name", "mohammad"))
}

Retrieving Cause

The package aims to incorporate a Cause() function that will assist you in traversing the error chain and retrieving the root cause error.

Handling Multiple Errors

In scenarios where you encounter multiple errors concurrently, the errors.MultiError type offers a seamless way to aggregate them without resorting to cumbersome string concatenation. It also ensures thread safety for concurrent operations:

err := errors.NewMultiError(io.EOF, errors.New("getting data"))

// Real Usage: when calling a third party service I need to check if error is timeout signal the up layer and do not want them notify timeout using lower level code, but my timeout. 
err := callingHttpCode()
if os.IsTimeout(errors.Cause(err)) {
    return res, errors.NewMultiError(err, constants.ErrTimeout)
}

Real Usage: some time happened you need to create list of error from multiple goroutine.

//
errorList := errors.NewMultiError()
wg := &sync.WaitGroup{}

wg.Add(1)
go func(){
	err := callingHttpClient()
	errorList.SafeAdd(err) // if result of `callingHttpClient` is nil no error will add to the err list.

	wg.Done()
}()

wg.Add(1)
go func(){
    err := callingHttpClient()
	if err != nil {
        errorList.SafeAdd(err)
    }

	wg.Done()
}()

wg.Wait()

if err := errorList.Err(); err != nil {
	// oh, something bad happened in one of routines above.
}

Combining sync.WaitGroup and errors.MultiError

The errors.WaitGroup type simplifies error handling in concurrent operations by merging sync.WaitGroup with MultiError. This reduces boilerplate code for cleaner and more concise error handling:

wg := errors.NewWaitGroup() // you can pass some options, e.g: your custom sync.WaitGroup using WaitGroupWithSyncWaitGroup.

wg.Add(1)
go func(){
    wg.Done(callingHttpClient())
}()

wg.Add(1)
go func(){
    err := callingHttpClient()
	wg.Done(err)
}()



if err := wg.Wait(); err != nil {
    // oh, something bad happened in one of routines above.
} 

or you can use Do method and let WaitGroup handle Add and Done internally.

wg := errors.NewWaitGroup() 

wg.Do(func(ctx context.Context) error {
    return callingHttpClient()
})

wg.Do(func(ctx context.Context) error {
    return callingHttpClient()
})


if err := wg.Wait(); err != nil {
    // oh, something bad happened in one of routines above.
} 

limiting the concurrent task counts using limit Options in errors.WaitGroup

wg := errors.NewWaitGroup(errors.WaitGroupWithTaskLimit(2)) 

wg.Do(func(ctx context.Context) error {
    return callingHttpClient()
})

wg.Do(func(ctx context.Context) error {
    return callingHttpClient()
})

// we set limit concurrent task to 2, so this task will block until one of above are done.
wg.Do(func(ctx context.Context) error {
	return callingHttpClient()
})

if err := wg.Wait(); err != nil {
    // oh, something bad happened in one of routines above.
} 

Use Custom runner instead of GoRoutine in errors.WaitGroup

import (
    "github.com/mrsoftware/errors"
    "github.com/panjf2000/ants/v2"
)

// in this example we are using ants goroutine pool.
wg := errors.NewWaitGroup(errors.WaitGroupWithTaskRunner(ants.Submit)) 

wg.Do(func(ctx context.Context) error {
    return callingHttpClient()
})

wg.Do(func(ctx context.Context) error {
    return callingHttpClient()
})
 
if err := wg.Wait(); err != nil {
    // oh, something bad happened in one of routines above.
} 

Stop all tasks on first error

import (
    "github.com/mrsoftware/errors"
)

// in this example we are using ants goroutine pool.
wg := errors.NewWaitGroup(errors.WaitGroupWithStopOnError(), errors.WaitGroupWithContext(context.Background())) // you can pass your own context.

wg.Do(func(ctx context.Context) error {
    return callingHttpClient(ctx)
})

wg.Do(func(ctx context.Context) error {
    return callingHttpClient(ctx)
})
 
// if one of above task failed, context will cancel and other task will stop (the task must ba aware of context cancellation like http pkg do)

if err := wg.Wait(); err != nil {
    // oh, something bad happened in one of routines above.
} 

Turn WaitGroup into a chanel

import (
    "github.com/mrsoftware/errors"
)

wg := errors.NewWaitGroup() 

wg.Do(func(ctx context.Context) error {
    return callingHttpClient()
})

wg.Do(func(ctx context.Context) error {
    return callingHttpClient()
})

// this wait until receive something from chanel.
var err error
select {
case err = <-errors.WaitChanel(wg):
}

if err != nil {
    // oh, something bad happened in one of the routines above.
} 

for mode details, check the documentation


Roadmap

  • Unit test
  • complete toolbox
  • Multi error
  • Waiting error (sync.Waiting + errors)