We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Multiple Errors

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.

Joining Errors

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.

Logging Multi-Errors

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.

Assignment

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:

  • A message attribute with the error's message
  • Any linkoerr attributes that can be extracted from the error
  • The stack_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.