My approach to handle errors in multi-tier applications in Go.
Find a file
2022-09-29 20:07:49 +02:00
api Initial commit 2022-09-25 18:39:24 +02:00
domain Initial commit 2022-09-25 18:39:24 +02:00
models Initial commit 2022-09-25 18:39:24 +02:00
persistence Initial commit 2022-09-25 18:39:24 +02:00
.gitignore Initial commit 2022-09-25 18:38:02 +02:00
go.mod Initial commit 2022-09-25 18:39:24 +02:00
LICENSE Initial commit 2022-09-25 18:38:02 +02:00
main.go Initial commit 2022-09-25 18:39:24 +02:00
README.md Typo 2022-09-29 20:07:49 +02:00

Handling errors in a multi-tier Go application

This repository shows my preferred way of proper error handling for a 3-tier application (API, domain, persistence). Errors send to API consumers are comprehensible and do not leak internals. For developers of the application, an error is of course comprehensible as well, but in addition enough information is provided to trace down the root of the problem.

Errors are only logged on domain layer. To the best of my knowledge, the calling function should be the one logging errors. In case of an imaginary request, the calling function is on API layer. But since an application can provide multiple API endpoints, say REST and gRPC, we would need to implement those log calls twice. Logging on domain layer seems to be the better choice since we only need to log once.

Explanation

I wanted to improve the way I handle errors in Go code I'm writing and read some articles about best practices, history etc.. The result including my personal preferences are written in code in this repo.

My main requirements for handling errors are:

  • Do not leak internals to API consumers
  • Provide context for developers to easily find the code causing an error
  • Applicable for layered applications that are bubbling up errors
  • No unnecessary log message duplicates

Project

I tried to write the code in this repository as easy as possible. That means that there is a lot missing, but I tried my best to include the most important parts necessary for applications with multiple layers and APIs.

API

In my case just a proxy to the domain layer. In a real world project, this layer could provide a GraphQL/REST/gRPC/... API. It is intended to be as simplistic as possible. I was thinking about the right place to convert internal errors to user-facing errors. API or domain layer were my choices. I decided to do this logic in the domain layer. The reason is, that maybe multiple API endpoints are provided. Hence, converting errors on API layer would need to be handled multiple times.

Domain

Contains domain logic, in this repo it is calling functions on persistence. In case of an error, the error is logged and an appropriate new error is returned. By returning a separate error we make sure that no internals are revealed. Using errors.Is and errors.As allows to return contextual appropriate error.

Persistence

Responsible for a simulated retrieval of users from an imaginary database. It is possible that database-specific errors occur. Those errors are wrapped with a bit of context and returned.

func F() error {
  return fmt.Errorf("package.Function: [%w]", err)
}

Wrapping the original error preserves all details and adds additional information. If necessary, calling functions can unwrap the error.