Domain-Centric Approach to Error Handling using Go (golang)

Alexander Demin
10 min readAug 15, 2023

--

TL;DR: The article discusses the domain-centric approach to error handling in general and specifically in Go programming language. This method treats errors as important parts of the application, rather than just technical issues. While it offers clear and user-focused error messages, it can also introduce complexity, especially for smaller projects. Balancing user experience, Golang’s simplicity, and the needs of the project is crucial when considering this approach.

Check out the next part of the error handling series: Context Matters: Advanced Error Handling Techniques in Go

Introduction

In the vast landscape of software development, errors have traditionally been seen mostly as technical glitches. However, with the evolution of programming paradigms, there’s a growing emphasis on understanding errors as integral domain components. This article delves deep into this shift in perspective, its implications, and its practical implementation in Golang.

The Historical Context: Errors as Technical Glitches

Historically, errors were often an afterthought in the software development process. When users encountered issues, they were presented with generic, unhelpful messages such as “Operation Failed” or “Something Went Wrong.”. Even today, many backend services return a simple “500” status code to a browser, which doesn’t provide clear information about the real cause how it can be solved. This approach had several significant drawbacks:

  • User Uncertainty: Generic error messages often left users in a state of confusion. They couldn’t determine if the problem stemmed from their actions, the server’s response, or another external factor.
  • Developer Challenges: Such vague errors posed significant challenges for developers. Without a clear context, debugging became a daunting task. To compensate, developers might resort to extensive logging. However, this often led to more confusion as it became difficult to differentiate log entries from various requests and trace the exact sequence of events.
  • Support Strain: The lack of clarity in error messages invariably led to an influx of queries to the customer support teams. These teams faced the arduous task of decoding the real issues behind these generic notifications.
  • Business Repercussions: Indistinct errors could discourage users from proceeding with their activities on the platform. This could result in potential revenue setbacks. Furthermore, if users perceived the platform as unreliable, it could adversely affect its reputation.

A Paradigm Shift: Recognizing Errors as Domain Components

The limitations of the traditional approach to error handling became increasingly evident as software development matured. Influenced by the principles of Domain-Driven Design (DDD), there was a shift in perspective. Developers began to realize that errors weren’t just technical issues but reflected real-world challenges and scenarios users face.

For instance, an “Out of Stock” error isn’t merely a system state. It represents a tangible business scenario that demands clear communication to the user.

The Importance of Structuring Errors

Embracing the idea of errors as domain components necessitates a structured approach to error classification. This structure ensures that errors are not only technically accurate but also meaningful to the end-users. Here’s a detailed breakdown of potential error categories and their significance:

  • Not Found: This category is essential for dynamic platforms where resources might go out of stock or pages get removed. It informs users that what they’re looking for isn’t available, ensuring they don’t waste time searching for non-existent resources.
  • Precondition Failed: This category handles scenarios where prerequisites aren’t met. For instance, many e-commerce promotions might require a minimum purchase amount. By classifying such errors, users can instantly recognize what’s missing and what they need to do to proceed.
  • Validation Error: With users inputting data, from search queries to payment details, validation errors are inevitable. They guide users on correct data entry, ensuring smooth transactions and interactions.
  • Unauthorized & Unauthenticated: These categories address user permissions and access rights. They ensure users know their boundaries on the platform, promoting a sense of security and trust.
  • Internal & Unknown: While the goal is to minimize internal errors, they do occur. And for those unforeseen errors that don’t fit predefined categories, an “Unknown” category captures them, ensuring that no issue goes unnoticed.

A common question that arises when discussing domain errors is: How do they differ from the HTTP or gRPC codes that have been in use for years? The distinction is crucial.

Infrastructure layer codes, like HTTP status codes, weren’t conjured out of nowhere. They were designed to convey the specific state or situation that occurred within a backend service. However, a key aspect that’s often overlooked is the origin of these status codes. The infrastructure layer, such as an HTTP request handler, isn’t the true source of the status code. Instead, it merely acts as a messenger, relaying a particular state or error that originated elsewhere in the service. This could be in the domain layer, a database repository, or even a database driver.

What’s important to understand is that the domain shouldn’t be burdened with raw infrastructure level status codes. Instead, the domain should offer detailed information about the error or state. This information empowers parts of the infrastructure layer, like an HTTP handler, to interpret the error and then communicate it effectively to the client with an appropriate status code.

In essence, while both domain errors and infrastructure status codes serve to communicate issues, their roles and origins are distinct. Recognizing this difference ensures clearer communication and a more structured approach to error handling.

Implementing Domain-Centric Error Handling in Golang

Golang, with its reputation for efficiency and performance, has been the language of choice for many developers. Its simplicity and clarity have made it a favorite for building scalable and high-performance systems. However, like all languages, it has its nuances, and one area that often sparks debate is its approach to error handling.

At its core, Golang’s error handling mechanism is fundamentally a system of typed strings. The built-in error interface is incredibly simple, essentially encapsulating a method that returns a string. This simplicity, while making the language accessible to newcomers, can sometimes be a limitation when dealing with complex applications, especially those that require nuanced error handling across different layers.

In many tutorials, articles, and even official documentation, error handling in Golang is often distilled down to a few basic methods. The ubiquitous errors.New(...) and fmt.Errorf(...) are frequently touted as the primary tools in a developer’s error-handling arsenal. These methods, while straightforward, essentially revolve around string manipulation. They allow developers to create and format error messages but offer little in terms of structured error handling or categorization.

In real-world applications, especially those with complex business logic and multiple layers, errors are not just strings. They represent specific scenarios, states, or conditions within the application’s domain. Treating them as mere strings can lead to ambiguity, making it challenging to diagnose issues, communicate them effectively to users, or handle them consistently across different parts of the application.

For Golang to truly shine in the realm of error handling, a more sophisticated abstraction is needed. Developers need tools and strategies that allow them to define, categorize, and handle domain-specific errors in a structured and consistent manner. This approach would elevate errors from being mere technical glitches to integral components of the application’s domain, ensuring that they are treated with the importance they deserve.

Lets delve deeper into strategies and implementations that can make domain errors in Golang be a first class citizen. To start, we’ll define a GenericError which will serve as the foundational structure for most of our domain error types:

type GenericError struct {
Code string
Message string
}

With this in place, we can define specific error types. For instance, a simple ValidationError can be structured as:

type ValidationError struct {
GenericError
}

func NewValidationError(code, msg string) *ValidationError {
return &ValidationError{GenericError{
Code: code,
Message: msg,
}}
}

Consider a domain layer validation function. It’s crucial to remember that validation is inherently a domain concern, and thus, it should reside within the domain layer. Here’s an example:

// ErrSomethingIsEmpty is a representation of the specific domain error 
// and can be defined on a package level or in some function as well.
var ErrSomethingIsEmpty = NewValidationError("something_is_empty",
"something should have a specific value and cannot be empty")

func (e *DomainEntity) DoSomething(val string) error {
if val == "" {
return ErrSomethingIsEmpty
}

// do the work …

return nil
}

Just like Validation errors, other error types can be created and used in a similar way. At the same time it’s important that every error has its own unique type. This makes the system more adaptable since different error types can give extra specific details, besides just the code and message.

Handling Domain Errors in the Infrastructure Layer

How we tell clients about errors is very important. Our domain layer has many error types, but the infrastructure layer changes these errors into clear messages for clients (other services, mobile applications, browsers). Let’s look at how this works, using an HTTP handler as an example.

Consider the following Golang implementation:

// handleRequest is some http request handler in infra layer
func handleRequest(r http.Request, w http.ResponseWriter) {
err := domainService.DoSomething()
if err != nil {
writeError(err, w)
return
}
}

// writeError is a function which translates domain errors to status codes
func writeError(err error, w http.ResponseWriter) {
var errAuth *domain.UnauthenticatedError
if errors.As(err, &errAuth) {
writeTypedError(w, http.StatusUnauthorized, errAuth)
return
}

var errNotFound *domain.NotFoundError
if errors.As(err, &errNotFound) {
writeTypedError(w, http.StatusNotFound, errNotFound)
return
}

// … continue with other error types

// when no typed error received, return generic internal error
// since we don't return any details to a client, it makes sense
// to log actual error so the error context is persisted.
writeTypedError(w, http.StatusInternalServerError, domain.ErrInternal)
}

// httpError is an example error JSON representation to return.
type httpError struct {
Code string `json:"code"`
Message string `json:"message"`
}

// DomainError is an interface all domain errors should implement.
type DomainError interface {
Code() string
Message() string
Error() string
}

// writeTypedError writes domain error to the response writer.
func writeTypedError(w http.ResponseWriter, code int, domainErr DomainError) {
errPayload := &httpError{
Code: domainErr.Code(),
Message: domainErr.Message(),
}

body, _ := json.Marshal(errPayload)
w.WriteHeader(code)
w.Write(body)
}

While this example is streamlined for simplicity, it encapsulates the essence of the approach. The writeError function checks the type of domain error and then delegates the task of writing the HTTP response to writeTypedError. This function, in turn, crafts a structured JSON response that’s sent to the client.

The beauty of this approach lies in its flexibility. If domain errors carry additional information, our HTTP error writer can be tailored to handle each type uniquely, crafting distinct payloads for the client. However, the specifics of these payloads often hinge on the contract established between the frontend and backend. It’s a dance of collaboration, ensuring that both sides understand and agree on the error messages and their meanings.

Benefits of Domain-Centric Error Handling

Adopting a domain-centric approach to error handling offers many advantages that go beyond just technical improvements. Here’s a deeper exploration of the benefits:

  • User Empowerment. Clear error messages not only reduce user confusion but also empower them to take corrective actions. When users understand the nature of the problem, they can often resolve minor issues on their own, leading to a more satisfying user experience.
  • Operational Efficiency. Precise error categorization allows support teams to prioritize and address issues more effectively. Instead of spending time deciphering vague error messages, they can jump straight to solutions. This efficiency can lead to quicker resolutions, happier customers, and reduced operational costs.
  • Business Impact. Transparent and clear communication can significantly enhance user trust. When users feel that a platform communicates effectively, even in the face of errors, they are more likely to remain loyal. This trust can lead to increased sales, better user retention, and a positive brand reputation.
  • Proactive Problem Solving. By treating errors as domain components, development teams can often anticipate common user challenges. This proactive approach means that potential issues can be addressed even before they become widespread problems, ensuring smoother user experiences.
  • Enhanced Developer Productivity. For developers, understanding errors as domain components can streamline the debugging process. Instead of sifting through layers of code to identify issues, they can focus on specific domain-related problems, making the development process more efficient.
  • Data-Driven Improvements. Domain-centric error handling can also provide valuable data insights. By analyzing the frequency and type of domain errors, businesses can identify areas that need improvement. For instance, if a particular error category frequently appears, it might indicate a broader systemic or UX issue that needs addressing.
  • Strengthened Security. Clear error categorizations, such as “Unauthorized” or “Unauthenticated,” can also play a crucial role in platform security. By promptly informing users of permission-related issues, platforms can prevent potential security breaches and ensure that user data remains protected.

In essence, domain-centric error handling is not just about improving the technical aspects of a platform. It’s about enhancing the entire ecosystem, from user experience to business operations. By recognizing the multifaceted benefits of this approach, platforms can position themselves for long-term success and resilience in a competitive digital landscape.

Conclusion

Using the domain-centric way to handle errors in Golang has many good points. It can make things clear and help users understand better. But, like everything, it’s important to look deeper and understand it fully. Let’s dive into some areas that need more attention:

  • The Challenge of Being Too Complex. The domain-centric way can make things more complicated. This means it can be hard to use for small projects. Sometimes, it’s simply too much. Before using this method, think about what your project really needs. The best way is often the easiest way that does the job.
  • The Balance of User Experience. This method wants to give clear messages when there’s an error. This is good because users understand better. But, sometimes, it’s not good to give too much information. For example, for safety reasons, it might be better to give a simple message. The challenge is to find a balance. We want to help the user but also keep things safe and simple.
  • Learning and Golang’s Simple Way. Golang is known for being simple and easy. For people new to Golang, the domain-centric way might seem hard. If you choose this method, make sure everyone understands it properly, so some training and mentoring might be needed. Remember, Golang likes to keep things simple.

Overall, using the domain-centric method in Golang lets developers build systems that are strong technically, focused on the user, and good for business. As the world of software keeps changing, this way of doing things will help shape how users feel and help businesses succeed.

But still it worth to remember, the domain-centric way has both benefits and challenges. Always think about what’s best for your project. It’s key to pick a method that supports users and matches Golang’s straightforward approach.

Check out the next part of the error handling series: Context Matters: Advanced Error Handling Techniques in Go

--

--