efmt

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 2 Imported by: 0

README

efmt

tag Go Version Go Reference Build Status Go report Coverage License

efmt is a Go library for enhanced error handling that allows you to attach structured metadata (key-value pairs) to errors. This helps to enrich error context while maintaining compatibility with Go's standard error interface.

Installation

go get github.com/vasilesk/efmt@latest

Features

  • Create errors with structured metadata
  • Wrap existing errors with additional context
  • Extract metadata from errors as key-value pairs
  • Collect metadata from the whole error tree, including fmt.Errorf("%w") and errors.Join
  • Pass metadata to log/slog
  • Fully compatible with Go's standard error interface and unwrap mechanism
  • No dependencies outside the standard library

Usage Examples

Creating a new error with metadata
import "github.com/vasilesk/efmt"

func validateUser(user User) error {
    if user.Age < 18 {
        return efmt.New("user is underage",
            efmt.KV("user_id", user.ID),
            efmt.KV("age", user.Age),
            efmt.KV("required_age", 18),
        )
    }
    return nil
}
Wrapping an existing error
import "github.com/vasilesk/efmt"

func processRequest(req Request) error {
    user, err := fetchUser(req.UserID)
    if err != nil {
        return efmt.Wrap(err, "failed to fetch user",
            efmt.KV("request_id", req.ID),
            efmt.KV("user_id", req.UserID),
        )
    }
    // Process user...
    return nil
}
Extracting metadata from errors
import (
    "fmt"
    "github.com/vasilesk/efmt"
)

func handleError(err error) {
    // Get all key-value pairs
    allValues := efmt.Values(err)
    fmt.Printf("Error context: %+v\n", allValues)

    // Get a specific value
    if requestID, ok := efmt.Value[string](err, "request_id"); ok {
        fmt.Printf("Request ID: %s\n", requestID)
    }

    // Get all key-value pairs as a slice
    pairs := efmt.ValuePairs(err)
    for _, kv := range pairs {
        fmt.Printf("%s: %v\n", kv.Key, kv.Value)
    }
}
Working with error chains
import "github.com/vasilesk/efmt"

func processData() error {
    data, err := fetchData()
    if err != nil {
        return efmt.Wrap(err, "data fetch failed",
            efmt.KV("timestamp", time.Now()),
        )
    }

    err = validateData(data)
    if err != nil {
        return efmt.Wrap(err, "data validation failed",
            efmt.KV("data_size", len(data)),
        )
    }

    return nil
}

// Later when handling the error:
func handleProcessingError(err error) {
    // This will extract all metadata from the entire error chain
    allValues := efmt.Values(err)
    fmt.Printf("Complete error context: %+v\n", allValues)
}
Logging with slog

efmt.Attrs returns the metadata as slog attributes in a stable order:

import (
    "context"
    "log/slog"

    "github.com/vasilesk/efmt"
)

func logError(ctx context.Context, logger *slog.Logger, err error) {
    attrs := append(efmt.Attrs(err), slog.Any("error", err))
    logger.LogAttrs(ctx, slog.LevelError, "request failed", attrs...)
}

Key precedence

When the same key is set at several levels of an error tree, the outermost value wins:

inner := efmt.New("parse failed", efmt.KV("stage", "parse"), efmt.KV("line", 7))
outer := efmt.Wrap(inner, "load failed", efmt.KV("stage", "load"))

efmt.Values(outer) // map[line:7 stage:load]

ValuePairs returns all pairs, duplicates included, in traversal order: an error comes before the errors it wraps, and errors combined with errors.Join are visited in order. Values, Value and Attrs all use the first occurrence of a key in that order. Value[T] returns false if that occurrence is not of type T; it does not fall back to a shadowed inner value.

Compatibility with the errors package

efmt errors work with errors.Is, errors.As and errors.Unwrap, and metadata is collected through errors wrapped by other means:

import (
    "errors"
    "fmt"

    "github.com/vasilesk/efmt"
)

err := fmt.Errorf("handler: %w", efmt.Wrap(ErrNotFound, "lookup failed", efmt.KV("id", 42)))

errors.Is(err, ErrNotFound) // true
efmt.Values(err)            // map[id:42]

License

See the LICENSE file for details.

Documentation

Overview

Package efmt attaches structured key-value data to errors.

Errors are created with New or wrapped with Wrap, passing pairs built by KV. The data can later be read back from anywhere in the error tree with Values, Value, ValuePairs or, for logging, Attrs.

Errors produced by this package work with errors.Is, errors.As and errors.Unwrap. Values are also collected through errors that were wrapped by other means, such as fmt.Errorf with %w or errors.Join.

When the same key is set at several levels, the outermost value wins: wrapping an error can override what was attached closer to its origin.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Attrs added in v1.1.0

func Attrs(err error) []slog.Attr

Attrs returns the values attached to err and the errors it wraps as slog attributes. Keys are deduplicated with the same precedence as Values, and attributes are ordered by the first occurrence of their key in ValuePairs, so the output is stable.

Example
package main

import (
	"context"
	"errors"
	"log/slog"
	"os"

	"github.com/vasilesk/efmt"
)

var errNotFound = errors.New("not found")

func findUser(id int) error {
	return efmt.Wrap(errNotFound, "user lookup failed", efmt.KV("user_id", id))
}

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey {
				return slog.Attr{}
			}

			return a
		},
	}))

	err := efmt.Wrap(findUser(42), "request failed", efmt.KV("request_id", "abc"))

	attrs := append(efmt.Attrs(err), slog.Any("error", err))
	logger.LogAttrs(context.Background(), slog.LevelError, "request failed", attrs...)
}
Output:
level=ERROR msg="request failed" request_id=abc user_id=42 error="request failed: user lookup failed: not found"

func New

func New(text string, kv ...KeyValue) error

New creates a new formatted error with the given text and optional key-value pairs.

Example
package main

import (
	"fmt"

	"github.com/vasilesk/efmt"
)

func main() {
	err := efmt.New("user is underage", efmt.KV("age", 16), efmt.KV("required_age", 18))

	fmt.Println(err)
	fmt.Println(efmt.Values(err))
}
Output:
user is underage
map[age:16 required_age:18]

func Value

func Value[T any](err error, key string) (T, bool)

Value retrieves a typed value for a specific key from the error chain. As with Values, the outermost occurrence of the key is used. It returns false if the key is not found or if that value is not of type T; in the latter case inner values with the same key are not consulted.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/vasilesk/efmt"
)

var errNotFound = errors.New("not found")

func findUser(id int) error {
	return efmt.Wrap(errNotFound, "user lookup failed", efmt.KV("user_id", id))
}

func main() {
	err := findUser(42)

	if id, ok := efmt.Value[int](err, "user_id"); ok {
		fmt.Println("user id:", id)
	}

	if _, ok := efmt.Value[string](err, "user_id"); !ok {
		fmt.Println("user id is not a string")
	}
}
Output:
user id: 42
user id is not a string

func Values

func Values(err error) map[string]any

Values extracts all key-value pairs from an error chain and converts them to a map. When multiple errors contain the same key, the value that ValuePairs returns first is used, i.e. the one from the outermost error.

Example (Join)
package main

import (
	"errors"
	"fmt"

	"github.com/vasilesk/efmt"
)

func main() {
	err := errors.Join(
		efmt.New("first", efmt.KV("first", 1)),
		fmt.Errorf("second: %w", efmt.New("inner", efmt.KV("second", 2))),
	)

	fmt.Println(efmt.Values(err))
}
Output:
map[first:1 second:2]
Example (Override)
package main

import (
	"fmt"

	"github.com/vasilesk/efmt"
)

func main() {
	inner := efmt.New("inner", efmt.KV("stage", "parse"), efmt.KV("line", 7))
	outer := efmt.Wrap(inner, "outer", efmt.KV("stage", "load"))

	fmt.Println(efmt.Values(outer))
}
Output:
map[line:7 stage:load]

func Wrap

func Wrap(err error, text string, kv ...KeyValue) error

Wrap wraps an existing error with additional text and optional key-value pairs. It preserves the original error's context while adding new information. Returns nil if the provided error is nil.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/vasilesk/efmt"
)

var errNotFound = errors.New("not found")

func findUser(id int) error {
	return efmt.Wrap(errNotFound, "user lookup failed", efmt.KV("user_id", id))
}

func main() {
	err := efmt.Wrap(findUser(42), "request failed", efmt.KV("request_id", "abc"))

	fmt.Println(err)
	fmt.Println(errors.Is(err, errNotFound))
	fmt.Println(efmt.Values(err))
}
Output:
request failed: user lookup failed: not found
true
map[request_id:abc user_id:42]

Types

type KeyValue

type KeyValue struct {
	Key   string
	Value any
}

KeyValue is a single piece of structured data attached to an error.

func KV

func KV(key string, value any) KeyValue

KV creates a key-value pair for attaching to errors. This is a convenience function to create structured data entries.

func ValuePairs

func ValuePairs(err error) []KeyValue

ValuePairs extracts all key-value pairs from an error and its wrapped errors. The error tree is traversed depth-first, the same way errors.Is does it: an error comes before the errors it wraps, and errors combined with errors.Join or several %w verbs are visited in order.

Jump to

Keyboard shortcuts

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