Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Complete the `stacktrace` and `json` package READMEs with the APIs introduced in v1.3.0 and the unreleased conditional-capture and formatting features.

- **`ExtractAttrs` drops a redundant dedup map** ([#31]) — the breadth-first traversal no longer keeps a secondary `map[*attributed]bool` to guard against double-counting a shared `*attributed`. The primary `visited` set is keyed on pointer identity (`errptr.Get`), which for an `*attributed` is that same pointer, and is checked-and-set at dequeue time before attrs are collected — so a shared instance reachable through several paths was already deduplicated there, and the second map never actually prevented a duplicate. Removes the per-call map setup and the per-node `attributed` bookkeeping, simplifying the loop body (allocation count is unchanged — escape analysis already stack-allocated both maps). Behavior is identical, as the existing diamond/DAG dedup test confirms.
- **`stacktrace` subpackage no longer uses `reflect` or `unsafe`** ([#29], [#35]) — `extractCarrierClassifications` now goes through the public `errx.CarrierClassifications` accessor instead of reflecting into the unexported `carrier.classifications` field, mirroring the migration already done in the `json` subpackage ([#17]). A future rename of the unexported `carrier` type can no longer silently drop classification extraction during stack-trace traversal. No exported APIs or behavior change.
- **README overhaul** — examples now lead with the idiomatic single-call variadic form (classification, displayable message, and attributes attached in one `Wrap`/`ClassifyNew`) instead of multi-step builder-style code. Adds a "Why errx" section comparing errx with alternative libraries (`pkg/errors`, `cockroachdb/errors`, `joomcode/errorx`, `morikuni/failure`, and `eris`), plus new sections covering error aggregation (`errx.Join`), attribute redaction via `WithAttributeValueTransformer`, and non-slog logger integration via `ToKVArgs`.
Expand Down
70 changes: 70 additions & 0 deletions json/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,76 @@ Control whether standard (non-errx) errors in the error chain are included.
jsonBytes, _ := errxjson.Marshal(err, errxjson.WithIncludeStandardErrors(false))
```

### WithStackTrace

Include captured stack traces in the serialized output. The default is `true`; pass
`false` to omit the `stack_trace` field. This is separate from `WithMaxStackFrames(0)`,
which means no frame limit.

```go
jsonBytes, _ := errxjson.Marshal(err, errxjson.WithStackTrace(false))
```

### WithAttributes

Include structured attributes in the output. The default is `true`; pass `false` to omit
the `attributes` field, for example when attributes may contain sensitive data.

```go
jsonBytes, _ := errxjson.Marshal(err, errxjson.WithAttributes(false))
```

### WithSentinels

Include sentinel text in the output. The default is `true`; pass `false` to omit the
`sentinels` field when those messages are internal implementation details.

```go
jsonBytes, _ := errxjson.Marshal(err, errxjson.WithSentinels(false))
```

### WithAttributeValueTransformer

Rewrite or redact each attribute value during serialization. The transformer receives the
Comment thread
denisvmedia marked this conversation as resolved.
attribute key and value, and is called only when attributes are included.

Note that this only transforms values in the `attributes` field, not in `message`. For example,
serializing `errx.Attrs("token", "secret")` with this transformer still includes `"message": "token=secret"`.
Similarly, `WithAttributes(false)` only suppresses the `attributes` field, leaving the message intact.
Neither option guarantees that sensitive values are omitted from the serialized JSON if they are present in the error message.

```go
jsonBytes, _ := errxjson.Marshal(err, errxjson.WithAttributeValueTransformer(
func(key string, value any) any {
if key == "token" {
return "[redacted]"
}
return value
},
))
```

### WithStackTraceTrimPaths

Remove a prefix from each stack frame's file path. An empty prefix (the default) leaves
paths unchanged.

```go
jsonBytes, _ := errxjson.Marshal(err,
errxjson.WithStackTraceTrimPaths("/home/runner/work/myproject/"))
```

### WithMaxMessageBytes

Limit each serialized error message to at most `n` bytes. When truncation is needed, the
suffix `...(truncated)` is appended without splitting a UTF-8 character. If `n` is 14
bytes or less (the length of the suffix), a hard cut at the byte limit is used instead
without appending the suffix. The default `0` leaves messages unchanged.

```go
jsonBytes, _ := errxjson.Marshal(err, errxjson.WithMaxMessageBytes(256))
```

## JSON Structure

The serialized error has the following structure:
Expand Down
77 changes: 69 additions & 8 deletions stacktrace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ Optional stack trace support for errx errors.

## Overview

The `stacktrace` package extends `errx` with stack trace capabilities while keeping the core `errx` package minimal and zero-dependency. It provides two usage patterns:
The `stacktrace` package extends `errx` with stack trace capabilities while keeping the
core `errx` package minimal and zero-dependency. It provides three usage patterns:

1. **Per-error opt-in** using `Here()` as a `Classified`
2. **Automatic capture** using `stacktrace.Wrap()` and `stacktrace.Classify()`
2. **Automatic capture** using `stacktrace.Wrap()`, `stacktrace.Classify()` and
`stacktrace.ClassifyNew()`
3. **Conditional capture** using `WrapIf`, `ClassifyIf` or `HereIf` to avoid duplicate traces
when a cause already carries one.

## Installation

Expand Down Expand Up @@ -42,7 +46,10 @@ Use `stacktrace.Wrap()` or `stacktrace.Classify()` for automatic trace capture:
err := stacktrace.Wrap("operation failed", cause, ErrNotFound)

// Or with Classify
err := stacktrace.Classify(cause, ErrRetryable)
err = stacktrace.Classify(cause, ErrRetryable)

// Create and classify a new error
err = stacktrace.ClassifyNew("user record missing", ErrNotFound)
```

### Extracting Stack Traces
Expand All @@ -58,6 +65,35 @@ if frames != nil {
}
```

`Extract` returns the first trace in the chain. Use `ExtractAll` when several layers or
branches may carry traces; it returns them outermost first.

### Conditional Capture

`WrapIf` and `ClassifyIf` capture a trace only when the cause does not already carry one.
Their `*Depth` variants let callers choose the maximum capture depth. `HereIf` and
`HereIfDepth` return `nil` when a trace is already present, so they can be passed to
`errx.Wrap`, `errx.Classify` or `errx.ClassifyNew` without adding a duplicate. Use
`HasTrace` to check for a non-empty trace before choosing your own behavior.

```go
err := stacktrace.WrapIf("load user", cause)
if stacktrace.HasTrace(err) {
fmt.Println("trace is available")
}
```

### Formatting Traces

Errors returned by `stacktrace.Wrap`, `Classify` and `ClassifyNew` implement
`fmt.Formatter`. When an error carries a trace, `%+v` prints its message followed by the
stack frames in the `pkg/errors` style; `%v` and `%s` print only the message. `errx.Wrap`
and `errx.Classify` also format traces attached with `stacktrace.Here()`.

```go
fmt.Printf("%+v\n", err) // message followed by stack frames
```

## Integration with errx Features

Stack traces work seamlessly with all errx features:
Expand All @@ -84,14 +120,39 @@ fmt.Println("Has stack trace:", stacktrace.Extract(err) != nil)

### Functions

- `Here() errx.Classified` - Captures the current stack trace as a Classified
- `Extract(err error) []Frame` - Extracts stack frames from an error chain
- `Wrap(text string, cause error, classifications ...errx.Classified) error` - Wraps with automatic trace
- `Classify(cause error, classifications ...errx.Classified) error` - Classifies with automatic trace
- `Here() errx.Classified` captures a trace for use with `errx.Wrap`, `errx.Classify` or
`errx.ClassifyNew`.
- `HereDepth(depth int) errx.Classified` captures up to the requested number of frames.
- `Wrap(text string, cause error, classifications ...errx.Classified) error` and
`WrapDepth(text string, cause error, depth int, classifications ...errx.Classified) error`
wrap an error and capture a trace.
- `Classify(cause error, classifications ...errx.Classified) error` and
`ClassifyDepth(cause error, depth int, classifications ...errx.Classified) error`
add classifications and capture a trace.
- `ClassifyNew(text string, classifications ...errx.Classified) error` and
`ClassifyNewDepth(text string, depth int, classifications ...errx.Classified) error`
create and classify an error with a trace.
- `WrapIf(text string, cause error, classifications ...errx.Classified) error` and
`WrapIfDepth(text string, cause error, depth int, classifications ...errx.Classified) error`
capture only when the cause has no trace.
- `ClassifyIf(cause error, classifications ...errx.Classified) error` and
`ClassifyIfDepth(cause error, depth int, classifications ...errx.Classified) error`
classify and capture only when needed.
- `HereIf(cause error) errx.Classified` and `HereIfDepth(cause error, depth int) errx.Classified`
return `nil` when the cause already carries a trace.
- `Extract(err error) []Frame` returns the first trace in the error chain;
`ExtractAll(err error) [][]Frame` returns all traces, outermost first.
- `HasTrace(err error) bool` reports whether the chain contains a non-empty trace.

### Constants

- `DefaultMaxDepth` is 32. Non-positive depth values use this default.
- `MaxDepth` is 256. Larger requested depths are clamped to this ceiling.

### Types

- `Frame` - Represents a single stack frame with `File`, `Line`, and `Function` fields
- `Frame` represents a stack frame with `File`, `Line` and `Function` fields.
- `Tracer` is implemented by errors that expose captured frames through `Frames() []Frame`.

## Performance Considerations

Expand Down
Loading