diff --git a/go/core/error.go b/go/core/error.go index ba70d7f76e..12671dd6c5 100644 --- a/go/core/error.go +++ b/go/core/error.go @@ -37,11 +37,12 @@ type ReflectionError struct { // GenkitError is the base error type for Genkit errors. type GenkitError struct { - Message string `json:"message"` // Exclude from default JSON if embedded elsewhere - Status StatusName `json:"status"` - HTTPCode int `json:"-"` // Exclude from default JSON - Details map[string]any `json:"details"` // Use map for arbitrary details - Source *string `json:"source,omitempty"` // Pointer for optional + Message string `json:"message"` // Exclude from default JSON if embedded elsewhere + Status StatusName `json:"status"` + HTTPCode int `json:"-"` // Exclude from default JSON + Details map[string]any `json:"details"` // Use map for arbitrary details + Source *string `json:"source,omitempty"` // Pointer for optional + originalError error // The wrapped error, if any } // UserFacingError is the base error type for user facing errors. @@ -70,7 +71,6 @@ func (e *UserFacingError) Error() string { // NewError creates a new GenkitError with a stack trace. func NewError(status StatusName, message string, args ...any) *GenkitError { - // Prevents a compile-time warning about non-constant message. msg := message ge := &GenkitError{ @@ -78,6 +78,14 @@ func NewError(status StatusName, message string, args ...any) *GenkitError { Message: fmt.Sprintf(msg, args...), } + // scan args for the last error to wrap it (Iterate backwards) + for i := len(args) - 1; i >= 0; i-- { + if err, ok := args[i].(error); ok { + ge.originalError = err + break + } + } + errStack := string(debug.Stack()) if errStack != "" { ge.Details = make(map[string]any) @@ -91,14 +99,22 @@ func (e *GenkitError) Error() string { return e.Message } +// Unwrap implements the standard error unwrapping interface. +// This allows errors.Is and errors.As to work with GenkitError. +func (e *GenkitError) Unwrap() error { + return e.originalError +} + // ToReflectionError returns a JSON-serializable representation for reflection API responses. func (e *GenkitError) ToReflectionError() ReflectionError { errDetails := &ReflectionErrorDetails{} - if stackVal, ok := e.Details["stack"].(string); ok { - errDetails.Stack = &stackVal - } - if traceVal, ok := e.Details["traceId"].(string); ok { - errDetails.TraceID = &traceVal + if e.Details != nil { + if stackVal, ok := e.Details["stack"].(string); ok { + errDetails.Stack = &stackVal + } + if traceVal, ok := e.Details["traceId"].(string); ok { + errDetails.TraceID = &traceVal + } } return ReflectionError{ Details: errDetails, diff --git a/go/core/error_test.go b/go/core/error_test.go new file mode 100644 index 0000000000..8361aa02c4 --- /dev/null +++ b/go/core/error_test.go @@ -0,0 +1,23 @@ +package core + +import ( + "errors" + "testing" +) + +func TestGenkitErrorUnwrap(t *testing.T) { + original := errors.New("original failure") + + // Use INTERNAL instead of StatusInternal + gErr := NewError(INTERNAL, "something happened: %v", original) + + // Verify errors.Is works (this is the most important check) + if !errors.Is(gErr, original) { + t.Errorf("expected errors.Is to return true, but got false") + } + + // Verify Unwrap works directly + if gErr.Unwrap() != original { + t.Errorf("Unwrap() returned wrong error") + } +}