

0 / 2 embers
0 / 3000 xp
click for more info
Complete a lesson to start your streak
click for more info
Difficulty: 10
click for more info
Not enough gems
Cost: 6 gems
1: Logging Errors
incomplete
2: Stack Traces
incomplete
3: Slog Groups
incomplete
4: Handle Errors Once
incomplete
5: Adding Attributes
incomplete
6: Multiple Errors
incomplete
Back
ctrl+,
Next
ctrl+.
This lesson's interactive features are locked, please to keep using them
What should you do if an operation can generate multiple errors? For example, in a batch operation or when validating multiple fields in a struct. You could log each error as it happens, but that violates our one-log-per-event principle.
I recommend gathering all the errors, and logging them together as a single event.
The standard library's errors.Join function combines multiple errors into one. The returned error implements the Unwrap() []error method, which returns the original list of errors. This lets us treat multiple errors as a single error, while still being able to access the individual errors if needed.
This example processes a list of items:
func batchProcess(items []Item) error {
var errs []error
for _, item := range items {
if err := processItem(item); err != nil {
errs = append(errs, fmt.Errorf("item %v: %w", item.ID, err))
}
}
return errors.Join(errs...)
}
If processing an item fails, it appends the error to a slice. At the end, errors.Join combines all the errors into a single error. If there were no errors, errors.Join returns nil.
Suppose we have the following code to log errors:
err1 := errors.New("first bad thing happened")
err2 := errors.New("a second really bad thing happened")
err = errors.Join(err1, err2)
logger.Error("couldn't connect to server", "error", err)
Our log output will look something like this:
time=2009-11-10T23:00:00.000Z level=ERROR msg="couldn't connect to server" err="first bad thing happened\na second really bad thing happened"
... kinda messy.
A simple option is to update the ReplaceAttr function to check whether an error implements Unwrap() []error, and if so, log each individual error separately:
type multiError interface {
error
Unwrap() []error
}
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == "error" {
if me, ok := a.Value.Any().(multiError); ok {
var errAttrs []slog.Attr
for i, err := range me.Unwrap() {
errAttrs = append(errAttrs, slog.String(fmt.Sprintf("error_%d", i+1), err.Error()))
}
return slog.GroupAttrs("errors", errAttrs...)
}
}
return a
},
}))
But even this code still doesn't handle wrapped multi-errors, nor stack traces for individual errors.
There's no obviously correct way to handle all of these cases. I recommend keeping things simple by not nesting multiple errors if possible. When you do join errors, make sure they're simple ones.
Currently, List in internal/store/store.go returns the first error it encounters, which means the caller has no visibility into multiple failures – let's surface them all!
Report multiple failures as one structured event.
I refactored replaceAttr a bit by extracting some of the logic into a new func errorAttrs(err error) []slog.Attr which builds the attrs slice with:
message attribute with the error's messagelinkoerr attributes that can be extracted from the errorstack_trace attribute (only if the error is a stackTracer)Restart your server with LINKO_LOG_FILE=linko.access.log set:
LINKO_LOG_FILE=linko.access.log go run .
Run and submit the CLI tests from the root of the Linko repo.