gofr icon indicating copy to clipboard operation
gofr copied to clipboard

RFC: Standard Error Handling Utility Package in gofr

Open gizmo-rt opened this issue 5 months ago • 11 comments

Summary

This RFC proposes the introduction of a centralized error handling utility package within the gofr framework. At present, error handling across the framework is somewhat inconsistent and decentralized, with individual packages employing their own mechanisms for capturing and emitting errors. This proposal seeks to establish a standardized approach to error definition, propagation, and handling across the system, with the goal of enabling structured logging, consistent error categorization, and enhanced observability.

Motivation

  • Inconsistency: Varying error-handling patterns across packages, which can introduce challenges in maintaining and scaling the codebase.
  • Lack of standardisation: No unified severity model or error code mapping exists.
  • Instrumentation gaps: Disparate error formats hinder effective structured logging and telemetry.
  • Security: Risk of leaking sensitive information due to lack of standardised redaction protocols.
  • Developer friction: Absence of a shared error vocabulary and mechanism creates additional overhead for new and existing developers.

Goals

  • Provide a common language and model for errors.
  • Enable flexibility in defining and configuring application-specific errors.
  • Standardize severity levels to make errors more predictable and actionable.
  • Enable structured logging and instrumentation by making errors machine-readable and codified.
  • Separate internal errors from user-facing messages to avoid data leakage or confusion.

Design Requirements

  • Use standard Go error interface as the foundation.
  • Support a configuration schema for: - Error codes - Internal vs external classification - Severity levels (Info, Warning, Critical)
  • Allow application developers to extend base error definitions with domain-specific ones.

Error Model

A standard error object should include the following fields:

type AppError struct {
    // Unique error code, e.g., "USER_NOT_FOUND"
    Code        string                
    // user-facing
    ExternalMessage  string
    // developer-facing 
    InternalMessage string 
    Severity    SeverityLevel
    // Wrapped error, optional
    Cause       error     
    // Additional context (safe for logging)
    Meta        map[string]interface{} 
}

Severity levels could be defined as:


type SeverityLevel string

const (
    // Indicates a potential problem that needs attention, but is not immediately critical. Example: low disk space or high CPU usage
    SeverityWarning  SeverityLevel = "warning" 
    
    // Issue that could lead to data loss or significant service disruption requiring immediate action. Example: application crashes 
    SeverityError    SeverityLevel = "error" 

    // system-crashing error, requiring immediate action. Example: loss of database connection
    SeverityCritical SeverityLevel = "critical" 
)

Sample Usage

func GetUser(ctx context.Context, id string) error {
    user, err := db.FindUserByID(id)
    if err != nil {
        return apperrors.
               context(ctx).New(
            "USER_NOT_FOUND",
            "User does not exist.",
            "no rows found in user table for given id",
            apperrors.SeverityWarning,
            err,
            map[string]interface{}{"user_id": id},
        )
    }
    return nil
}

Outcomes

  • Centralized error handling
  • Better developer experience and readability
  • Minimized risk of leaking sensitive data
  • Easier evolution of the error-handling model over time

Next Steps

  • Build core utility package under pkg/errors in gofr.
  • Establish a error code registry (file or config).
  • Migrate modules to the new error model incrementally.
  • Create developer documentation and usage guidelines.

gizmo-rt avatar Jul 11 '25 07:07 gizmo-rt

Utility packages are an anti-pattern in go. (ref: https://go.dev/doc/effective_go)

vikash avatar Jul 15 '25 08:07 vikash

Using a utility package in Go is considered an anti-pattern, especially when it contains a mix of unrelated functions thrown together to avoid cyclic dependencies or simply because there isn't a better place to put them. However, if a package is focused on a specific feature, is useful, testable, and has a clear purpose, it aligns well with Go's design philosophy. A good example is the httputil package in the Go standard library.

gizmo-rt avatar Jul 15 '25 09:07 gizmo-rt

I feel like it's a trap to consider this.

Let's put aside the implementation you suggest for the moment.

  • exposing error struct/sentinel means you maintain them for ever
  • error struct should be considered when they are close to the function they use. But not a framework level like this
  • if there is a need to detect error outside gofr framework, it means there is a need to consider them inside the app that uses gofr
  • its possible to join a sentinel error to all gofr and it would be enough. But this could also be done by anyone calling gofr framework in a wrapper method. So I'm unsure there is a need for it.

About the implementation itself now, I'm not fan of such complicated thing.

I feel it's better to focus on defining what are the current problems when using gofr, and not over think how an global error format could be.

ccoVeille avatar Jul 24 '25 05:07 ccoVeille

The design of this package intentionally aims to keep the surface area minimal and semantically meaningful. Allow me to share more here

  • Exposing error types may translate to maintain them indefinitely but the idea here is to define a controlled set of common and reusable error types that represent categories rather than specific error cases. This improves consistency across the applications using gofr.

  • Defining local errors makes sense and we surely can encourage that. However at the framework level there is value in having standardized, composable error types that are shared across different layers of the stack. This becomes more important when multiple components (HTTP handler, middleware, logger, etc.) need to interpret or categorize them.

  • Also, the users could wrap gofr calls and inject sentinel errors themselves. However different teams might wrap errors differently, reducing consistency across services. By offering a standard error package, we provide a unified, framework endorsed approach that improves developer experience and reduces the burden on individual teams.

  • Enabling application level error detection without needing to couple tightly with internal gofr implementation. Currently, developers often need to resort to brittle string matching or duplicate logic to interpret errors returned from gofr methods.

The goal is to make error taxanomy consistent, clean , avoid duplication and keep a low surface area of maintainence.

gizmo-rt avatar Jul 24 '25 12:07 gizmo-rt

Having standardisation is helpful for sure; however, we should be vary of over generalisation. Error interfaces should be defined at the store, service and delivery layer separately so that they may serve the requisite purpose of the respective layer. The interfaces could also support conversion to other layer, i.e. from DBError to ServiceError and from ServiceError to HTTPError/ GrpcError, etc.

akshat-kumar-singhal avatar Jul 31 '25 06:07 akshat-kumar-singhal

Layered error packages introduce tight coupling and redundancy. Understanding and managing layered errors across packages is more complex. It leads to duplicated concepts - e.g., RecordNotFoundError, InvalidIdentifierError, NotFoundError which are all representing the same failure from different perspectives. This violates the DRY (Don't Repeat Yourself) principle and creates semantic clutter for developers.

Besides, layering makes backtracking harder and debugging becomes complicated. The solution isn't to fragment the design into multiple packages but allow flexibility without losing governance control.

gizmo-rt avatar Aug 11 '25 06:08 gizmo-rt

It leads to duplicated concepts - e.g., RecordNotFoundError, InvalidIdentifierError, NotFoundError which are all representing the same failure from different perspectives.

While they are similar errors from different perspectives, the behaviour/handling of them would be decided by the respective layer.

layering makes backtracking harder and debugging becomes complicated.

I'd disagree - layering helps define boundaries to limit the scope of debugging. A single entity which exists throughout the application appears very useful at first but can become a management nightmare. A common pattern in java is to let the exception bubble up to the top most layer and get handled there. This of course leads to high code complexity at a single place. I see our implementation proposal similar to the java style.

Origin of the error should decide what level of visibility is to be provided to the caller. Example: Service layer should not care about the underlying reason for an error returned by the store layer, unless the store layer wants to expose it such that the service layer can act on it.

akshat-kumar-singhal avatar Aug 11 '25 07:08 akshat-kumar-singhal

Can you share an example explaining the nature of layering expected here ? Helps provide more insights into it.

gizmo-rt avatar Aug 11 '25 09:08 gizmo-rt

Can you share an example explaining the nature of layering expected here ? Helps provide more insights into it.

Each layer (store, service, delivery) would have it's own error interface and corresponding errors (implementing the respective interfaces).

akshat-kumar-singhal avatar Aug 12 '25 05:08 akshat-kumar-singhal

@akshat-kumar-singhal Is this something like the following you had in mind ? If not, feel free to make updates

type StoreError interface { Code() string SubCode() string WithStatusCode(string) ErrorSchema WithMeta(map[string]any) ErrorSchema }

type ServiceError interface { Code() string SubCode() string WithStatusCode(string) ErrorSchema WithMeta(map[string]any) ErrorSchema }

type HandlerError interface { Code() string SubCode() string WithStatusCode(string) ErrorSchema WithMeta(map[string]any) ErrorSchema }

gizmo-rt avatar Aug 12 '25 07:08 gizmo-rt

something like this..

type StoreError interface {
  error
  getServiceError() ServiceError
  getRootCause() string
}

type ServiceError interface {
   error
  getHTTPError() HTTPError
  getGrpcError() GrpcError
  getRootCause() string
}

type HTTPError interface {
  error
  getRootCause() string
  getStatusCode() int
  getErrorMessage() string
}

akshat-kumar-singhal avatar Aug 13 '25 16:08 akshat-kumar-singhal