Skip to content

fix(errors): improve error handling ergonomics and docs#63

Merged
kooksee merged 2 commits into
v2from
fix/errors-improvements
Jun 25, 2026
Merged

fix(errors): improve error handling ergonomics and docs#63
kooksee merged 2 commits into
v2from
fix/errors-improvements

Conversation

@kooksee

@kooksee kooksee commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add tag helpers, FormatChain, MarshalError, and error chain traversal APIs
  • Fix ErrCode.As, WrapStack side effects, ErrJsonify chain expansion, and tag immutability
  • Integrate error chains into log.Err and result.Error
  • Expand errors/errcode documentation and examples

Test plan

  • go test ./errors/...
  • go test ./log/... ./result/...

Made with Cursor

kooksee and others added 2 commits June 25, 2026 21:28
Implement missing GetTags/CollectTags APIs, fix ErrCode.As and registry panics, remove WrapStack stderr side effects, and align errparser with structured errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add FormatChain, CollectUserTags, and errcode lookup APIs; align JSON serialization and wire error chains into log and result helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the errors package by introducing structured error chain formatting (FormatChain), metadata tag collection (CollectTags, CollectUserTags), and a new errcode sub-package for typed business error codes with protobuf/gRPC compatibility. It also updates logging and result integrations to leverage these new features. The review feedback highlights several critical areas for improvement: adding synchronization to prevent concurrent map access panics in the error code and parser registries, avoiding redundant message duplication in FormatChain when the wrap message matches the underlying error, ensuring GetTags supports both pointer and value types of Err, and adding a nil-pointer guard in ErrCode.As to prevent panics on zero-initialized error codes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +31 to +44
func RegisterErrCode(code *errorpb.ErrCode) error {
if code == nil {
return fmt.Errorf("errcode: code is nil")
}
if code.Name == "" {
return fmt.Errorf("errcode: code name is empty")
}
if errorCodes[code.Name] != nil {
panic(fmt.Sprintf("code exists, code=%s", errorCodes[code.Name]))
return fmt.Errorf("errcode: already registered: name=%q", code.Name)
}

errorCodes[code.Name] = proto.Clone(code).(*errorpb.ErrCode)
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The global map errorCodes is accessed concurrently by RegisterErrCode, LookupErrCode, and GetErrCodes without any synchronization. In Go, concurrent map writes and reads/writes will cause a runtime panic. Please introduce a sync.RWMutex to protect access to errorCodes.

Comment on lines 18 to 29
func RegisterParser(errParser ErrParser) bool {
key := stack.CallerWithFunc(errParser).String()
if errParserRegistry[key] != nil {
slog.Error("errParser already exists", "parser", errParserRegistry[key])
if errParser == nil {
slog.Error("errParser is nil")
return false
}

if errParser == nil {
slog.Error("errParser is nil")
key := stack.CallerWithFunc(errParser).String()
if errParserRegistry[key] != nil {
slog.Error("errParser already exists", "parser", errParserRegistry[key])
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The global map errParserRegistry is accessed concurrently by RegisterParser and parseError without any synchronization. This can lead to concurrent map read/write panics at runtime. Please use a sync.RWMutex or sync.Map to ensure thread safety.

Comment thread errors/format.go
Comment on lines +10 to +13
case *ErrWrap:
if msg, ok := e.Tags["msg"]; ok {
return strings.TrimSpace(fmt.Sprint(msg))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When WrapCaller or WrapStack wraps an error, they automatically set the "msg" tag to err.Error(). In FormatChain, this causes the root error message to be duplicated in the formatted chain (e.g., "init failed: db failed: db failed"). By checking if the "msg" tag is identical to the underlying error's message, we can filter out these automatic/redundant messages and keep the formatted chain clean.

Suggested change
case *ErrWrap:
if msg, ok := e.Tags["msg"]; ok {
return strings.TrimSpace(fmt.Sprint(msg))
}
case *ErrWrap:
if msg, ok := e.Tags["msg"]; ok {
msgStr := strings.TrimSpace(fmt.Sprint(msg))
if msgStr != "" && msgStr != strings.TrimSpace(e.Err.Error()) {
return msgStr
}
}
return ""

Comment thread errors/tags.go
Comment on lines +30 to +32
if e, ok := current.(*Err); ok && len(e.Tags) > 0 {
inner = e.Tags
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The GetTags function only checks for *Err (pointer type) when traversing the error chain. However, Err can also be used as a value type (as seen in errLayerData switch cases). To ensure robustness, please check for both *Err and Err types.

		if e, ok := current.(*Err); ok && len(e.Tags) > 0 {
			inner = e.Tags
		} else if e, ok := current.(Err); ok && len(e.Tags) > 0 {
			inner = e.Tags
		}

Comment on lines +249 to 252
func (t *ErrCode) As(target any) bool {
if target == nil {
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If ErrCode is zero-initialized (i.e., t.pb is nil), calling As can cause a panic during proto.Clone(t.pb).(*errorpb.ErrCode) due to type asserting a nil interface. Consider adding a guard check if t.pb == nil { return false } at the beginning of the As method.

Suggested change
func (t *ErrCode) As(target any) bool {
if target == nil {
return false
}
func (t *ErrCode) As(target any) bool {
if target == nil || t.pb == nil {
return false
}

@kooksee kooksee merged commit 7b0ee73 into v2 Jun 25, 2026
2 checks passed
@kooksee kooksee deleted the fix/errors-improvements branch June 25, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant