What is included in this blog:
- A discussion about how to do error handling in Golang.
Before reading this blog, I recommend you reading this doc to understand why it is recommended for functions that return errors always to use the error
interface (defined in $GOROOT/src/builtin
) other than concrete error types in their signature.
Suppose you have a micro-service called users-usvc
which is used to manage users in a system and you are adding an API to the micro-service for creating users in the system.
Here is the API specification
POST https://user.micro-service.com/users/v1/
Request Body:{FirstName: string, LastName: string, Password: string, Email: string}
Here is the pseudocode code of the Create
method in the userManager
object (which is used to manage users in the database):
// Create - the implementation of the `Create` method. It uses builtin errors to do the error handling
func (m *manager) Create(firstName, lastName, password, email string) (string, error) {
var ID string
if `the password containers some characters that the system can't recognize` {
return ID, fmt.Errorf("The password contains some invalid characters.")
}
if `a user with the given email already exists` {
return ID, fmt.Errorf("The email %s has been used by another user.", email)
}
ID, err := mysqlClient.CreateUser(firstName, lastName, password, email)
if err != nil {
return ID, fmt.Errorf("Error creating user {Name: %s %s, Email: %s}, err: %s", firstName, lastName, email, err.Error())
}
return ID, nil
}
Here is the pseudocode code of the API handler:
import (
...
userV1 "github.com/azhuox/blogs/golang/error_handling/users-usvc/internal/user/v1"
)
// CreateUserAPIHandler is the API handler for creating a site. It uses builtin errors to do the error handling
func CreateUserAPIHandler(w http.ResponseWriter, r *http.Request) {
var err error
user := &struct{
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
Password string `json:"phone"`
Email string `json:"email"`
}{}
// Parse args
if err = json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, fmt.Sprintf("Error decoding request params, err: %s", err.Error()), http.StatusBadRequest)
return
}
// Create a user manager
userManager, err := userV1.NewManager(...)
if err != nil {
log.Printf("[user_create_v1] error creating user manager, err: %s", err.Error())
http.Error(w, "Internal server error, please retry later", http.StatusBadRequest)
return
}
// Use the user manager to create a user with given parameters
ID, err := userManager.Create(user.FirstName, user.LastName, user.Password, user.Email)
if err != nil {
log.Printf("[user_create_v1] error creating the user %#v, err: %s", user, err.Error())
http.Error(w, "Internal server error, please retry later", http.StatusInternalServerError)
return
}
// Return ID
json.NewEncoder(w).Encode(&struct{ID string `json:"ID"`}{ID: ID})
w.WriteHeader(http.StatusOK)
return
}
Everything works fine in these code. However, one thing you may not be happy about is that the API handler always returns http.StatusInternalServerError
as status code for any error returned by the userManager.Create()
method. You may want to return different status codes based on different error types. The key point to solve this problem is to let the userManager.Create()
method return specific error types and let the API handler set the correct status code based on these errors.
The first solution utilizes Golang structs to define customized error types. Here is an example:
// baseErr - base class
type baseErr struct {
msg string
}
// Error implements the `Error` method defined in error interface
func (e *baseErr) Error() string {
if e != nil {
return e.msg
}
return ""
}
// newBaseErr creates an instance of internal error
func newBaseErr(format string, a ...interface{}) *baseErr {
return &baseErr {
msg: fmt.Sprintf(format, a...),
}
}
// BadRequestErr represents bad request errors
type BadRequestErr struct {
*baseErr
}
// newBadRequestErr creates an instance of BadRequestErr
func newBadRequestErr(format string, a ...interface{}) error {
return &BadRequestErr {
baseErr: newBaseErr(format, a...),
}
}
How to use these error types:
// Create - the implementation of the `Create` method. It uses the first solution to do error handling.
func (m *manager) Create(firstName, lastName, password, email string) (string, error) {
var ID string
if `the password containers some characters that the system can't recognize` {
return ID, newBadRequestErr("The password contains some invalid characters.")
}
if `a user with the given email already exists` {
return ID, newConflictErr("The email %s has been used by another user.", email)
}
ID, err := mysqlClient.CreateUser(firstName, lastName, password, email)
if err != nil {
return ID, newInternelServerErr("Error creating user {Name: %s %s, Email: %s}, err: %s", firstName, lastName, email, err.Error())
}
return ID, nil
}
Error handling in the API handler:
// CreateUserAPIHandler is the API handler for creating a site. It uses the first solution to do the error handling.
func CreateUserAPIHandler(w http.ResponseWriter, r *http.Request) {
var err error
... // A bunch of operations are omitted
// Use the user manager to create a user with given parameters
ID, err := userManager.Create(user.FirstName, user.LastName, user.Password, user.Email)
if err != nil {
log.Printf("[user_create_v1] error creating the user %#v, err: %s", user, err.Error())
switch err.(type) {
case *userV1.BadRequestErr:
http.Error(w, fmt.Sprintf("Bad request: %s", err.Error()), http.StatusBadRequest)
case *userV1.ConflictErr:
http.Error(w, fmt.Sprintf("Bad request: %s", err.Error()), http.StatusConflict)
case *userV1.InternelServerd5032592-c01e-4663-856a-2401ccee4c03Err:
http.Error(w, "Internal server error, please retry later.", http.StatusInternalServerError)
default:
http.Error(w, "Unknown error, please retry later.", http.StatusInternalServerError)
}
}
// Return ID
json.NewEncoder(w).Encode(&struct{ID string `json:"ID"`}{ID: ID})
w.WriteHeader(http.StatusOK)
return
}
- Return an
error
interface other than specific error types in the signature of theuserManager.Create()
method. You can read this doc for the reason. - Expose customized error types (make them public) so that callers can do error handling by converting an
error
interface to a specific error type. - Do not expose the
new
methods of those error types in order to make them read-only. Moreover, return anerror
interface in the signature of thesenew
methods' as well, as this converts an error type's pointer (say*userV1.BadRequestErr
) to anerror
interface. - Do the error handling by converting an
error
interface to a specific error type's pointer. The reason why this works is that theerr
returned by theuserManager.Create()
method is anerror
interface with a value and a type which essentially is an error type’s pointer (say*userV1.BadRequestErr
). Therefore,if _, ok := err.(*userV1.BadRequestErr); ok {...}
totally works as it just converts the interfaceerr
back to its type.
- It follows the principle of returning an
error
interface in a function's signature. - It provides a way for you to handle specific errors.
- Each error type can be customized. This allows you to add more details to an error type. The
SyntaxError
error type in Golang json package is a perfect example. It has a member calledOffset
which is used to indicate where the error occurred after reading bytes. Here is theSyntaxError
error type definition:
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
Here is an example of using the Offset
member:
if err := dec.Decode(&val); err != nil {
if serr, ok := err.(*json.SyntaxError); ok {
line, col := findLine(f, serr.Offset)
return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
}
return err
}
- Defining error types (with Golang structs) and those
new
methods are somehow overwhelmed. You need to crate a struct and anew
method for every error type. Plus, you can see from the example that, in some cases, we don't use an error type's methods or members, instead we only care about what the error type is. It is overwhelmed to use a struct to define an error type just for achieving this goal. - I personally don't like the idea of converting an
error
interface back to a specific error type. First, it somehow forces callers to figure out whether an error type or the error type's pointer is actually returned. For example,if _, ok := err.(*userV1.BadRequestErr); ok {...}
will not work if theuserManager.Create()
method returns aBadRequestErr
instead of*BadRequestErr
. This is because Golang is a strong type language, soBadRequestErr
does not equal to *BadRequestErr. Second, in my opinion, an interface is not supposed to be converted back to a specific type. This is because a Golang interface is designed for you to focus on some behaviors (which are methods defined in the interface) and ignore the implementation details. Converting an
error` interface back to a specific error type means you want to expose some implementation details, thus violating the principle that I just mentioned.
Instead of using Golang structs to define error types, the second solution extend the error
interface to a customized interface userV1.Error
by adding a Type()
method which returns specific error types. Here is the definition of the userV1. Error
interface and its implementation:
// Error interface defines the errors used in this package
type Error interface {
error
Type() ErrType
}
// errorImpl - implementation of Error interface
type errImpl struct {
msg string
errType ErrType
}
// Error returns error message
func (e *errImpl) Error() string {
if e != nil {
return e.msg
}
return ""
}
// Type returns error type
func (e *errImpl) Type() ErrType {
if e != nil {
return e.errType
}
return ErrTypeUnknown
}
// newError returns an error with given error type
func newError(errType ErrType, format string, a ...interface{}) Error {
return &errImpl{
msg: fmt.Sprintf(format, a...),
errType: errType,
}
}
// ConvertError - try converting an `error` interface to an `Error` interface
func ConvertError(err error) (Error, bool) {
if e, ok := err.(Error); ok {
return e, ok
}
return nil, false
}
Here is the definition of customized error types. You can see that the Golang structs defined in the first solution are replaced with constants in this solution.
// Bad request errors
const (
// ErrTypeBadRequest - bad request
ErrTypeBadRequest ErrType = "bad_request"
// ErrTypeConflict - resource conflicts
ErrTypeConflict ErrType = "conflict"
// ErrTypeInternalServerErr - internal server error
ErrTypeInternalServerErr ErrType = "internal_server_error"
// ErrTypeUnknown - Unknown error
ErrTypeUnknown ErrType = "unknown"
)
How to use these error types:
// Create - the implementation of the `Create` method. It uses the second solution to do error handling.
func (m *manager) Create(firstName, lastName, password, email string) (string, error) {
var ID string
if `the password containers some characters that the system can't recognize` {
return ID, newError(ErrTypeBadRequest, "The password contains some invalid characters.")
}
if `a user with the given email already exists` {
return ID, newError(ErrTypeConflict, "The email %s has been used by another user.", email)
}
ID, err := mysqlClient.CreateUser(firstName, lastName, password, email)
if err != nil {
return ID, newError(ErrTypeInternalServerErr, "Error creating user {Name: %s %s, Email: %s}, err: %s", firstName, lastName, email, err.Error())
}
return ID, nil
}
Error handling in the create a user
API handler
// CreateUserAPIHandler is the API handler for creating a site. It uses the second solution to do error handling.
func CreateUserAPIHandler(w http.ResponseWriter, r *http.Request) {
var err error
... // A bunch of operations are omitted
// Use the user manager to create a user with given parameters
ID, err := userManager.Create(user.FirstName, user.LastName, user.Password, user.Email);
if err != nil {
log.Printf("[user_create_v1] error creating the user %#v, err: %s", user, err.Error())
if uErr, ok := userV1.ConvertError(err); ok {
// Upgrade the `error` interface to the `userV1.Error` interface so that we can use the `Type()` method to get a concrete error type
switch uErr.Type() {
case userV1.ErrTypeBadRequest:
http.Error(w, fmt.Sprintf("Bad request: %s", uErr.Error()), http.StatusBadRequest)
case userV1.ErrTypeConflict:
http.Error(w, fmt.Sprintf("Bad request: %s", err.Error()), http.StatusConflict)
case userV1.ErrTypeInternalServerErr:
http.Error(w, "Internal server error, please retry later.", http.StatusInternalServerError)
default:
http.Error(w, "Unknown error, please retry later.", http.StatusInternalServerError)
}
} else {
// This should never happen
http.Error(w, "Unknown error, please retry later.", http.StatusInternalServerError)
}
}
// Return ID
json.NewEncoder(w).Encode(&struct{ID string `json:"ID"`}{ID: ID})
w.WriteHeader(http.StatusOK)
return
}
- The idea behind this solution is extending the
error
interface to a customizeduserV1.Error
interface with aType()
method which returns error types - the
userManager.Create()
method uses anuserV1.Error
interface instance other than an error type's pointer to record errors. This ensures no implementation details of theuserV1.Error
interface gets exposed. - The signature of
userManager.Create()
method still returns anerror
interface other than auserV1.Error
interface. This gives you the freedom to keep using the sameerror
interface instanceerr
created at the beginning of the API handler and allows you to do the conversion whenever you need. It is like we provide you with a great feature, but we do not force you to use it. - Those Golang structs in the first solution are replaced with the constants in this solution. Then callers of the
userManager.Create()
method can utilize these constants to handle different errors. - In the
userV1.ConvertError()
method, anerror
interface is upgraded to anuserV1.Error
interface when you need to parse errors returned by theuserManager.Create()
method.
- Return an
error
interface in the signature of theuserManager.Create()
method allows you to return either a regularerror
instance or auserV1.Error
instance. (Although you should always returnuserV1.Error
for any methods in theuserV1
package.) - It hides the details of how a private Golang struct (
userV1.errorImpl
) is defined to realize theuserV1.Error
interface and it only exposes what it wants to expose. - It is easier and has less work to define error types using constants other than structs.
- Customizing error types becomes impossible in this solution. This is because all the errors in this solution are constructed from the same Golang struct
userV1.errorImpl
and they all follow the constraint of theuserV1.Error
interface.
- It is recommended for functions that return errors always to use the
error
interface other than concrete error types in their signature. - Use structs to define error types and expose them if you need to customize some error types.
- Define a customized interface to extend the
error
interface if all the error types that you want to define have the same properties.
You can check the complete example from this repo.
That's it, thanks for reading this blog.
Reference