Categorygithub.com/lefinal/meh
modulepackage
1.14.0
Repository: https://github.com/lefinal/meh.git
Documentation: pkg.go.dev

# README

meh

made-with-Go Go GitHub go.mod Go version GoReportCard example codecov GitHub issues GitHub code size in bytes

Mastery of Error Handling

Convenient error handling for Go using custom error containers and bubbling features.

This document provides an overview over provided features but is not complete. Documentation available here.

Installation

In order to use this package, run:

go get github.com/lefinal/meh

What errors are made of

Errors consist of the following properties:

Code

Error codes that also describe the severity. When an error is created, the code is set. The following default codes ship with meh:

  • internal: Basic internal errors like a failed database query.
  • bad-input: Bad user input/request.
  • not-found: The requested resource could not be found.
  • unauthorized: Authentication is required for accessing the resource or performing the action.
  • forbidden: Invalid permissions for accessing the resource or performing the action.
  • neutral: Used for wrapping errors without changing the code.
  • (unexpected): No code specified.

Codes can be used for error handling based on the occurred problem. They may also be used for choosing HTTP status codes (see the mehhttp-package).

Of course, you can define custom codes. However, you should use the __-prefix in order to avoid collisions with codes being added natively in the future. Example: __myapp_my_code

Wrapped error

Errors are meant to be wrapped when being returned to the caller. This allows details to bubble up and be logged as well as the error message to be displayed in a stacktrace-like manner. If the error is a root error (the original cause), this property will not be set.

Message

The actual error message. This is a string and should describe the action that was performed.

Bad example: error while loading file

Good example: load file

The error messages will be concatenated with colons. Therefore, the final format with the original error wrapped two times looks like this:

get users: load user file: file not found

Details

Details are one of the main reasons why meh was developed. They are of type map[string]interface{} and allow passing arbitrary details as key-value pairs. Most times you will want to pass function call arguments here in order to allow easier inspection of issues. Because of wrapping, details are persisted and returned back all to the top caller which handles the error. If no details are provided, this can be kept unset (nil).

Creating errors

Errors can be created manually using the Error-struct:

return &meh.Error{
	Code: meh.ErrInternal,
	WrappedErr: err,
	Message: "read file",
	Details: meh.Details{
		"file_name": "my-file.txt"
    }
}

However, you want to use generators most times because of the syntactic sugar they provide.

General ones:

func NewErr(code Code, message string, details Details) error
func NewErrFromErr(err error, code Code, message string, details Details) error

These allow creating a new error with the given code, message and details. The ones with FromErr-suffix create a new error with the given one used as wrapped error.

Often, you use the native error codes. That's why there are generators, including codes:

func NewInternalErr(message string, details Details) error
func NewInternalErrFromErr(err error, message string, details Details) error
func NewBadInputErr(message string, details Details) error
func NewBadInputErrFromErr(err error, message string, details Details) error
func NewNotFoundErr(message string, details Details) error
func NewNotFoundErrFromErr(err error, message string, details Details) error
func NewUnauthorizedErr(message string, details Details) error
func NewUnauthorizedErrFromErr(err error, message string, details Details) error
func NewForbiddenErr(message string, details Details) error
func NewForbiddenErrFromErr(err error, message string, details Details) error

Wrapping errors

Most of the time, you do not want to create new error but wrap it for passing it over to the caller. This preserves the error code from the underlying error.

Let's have a look at an example which describes a situation where errors are wrapped:

struct Fruit {
	// ...
}

func IncrementApplesForUser(userID uuid.UUID, includePineapples bool) error {
	apples, err := applesByUser(userID, includePineapples)
	if err!= nil {
		return meh.Wrap(err, "apples by user", meh.Details{
			"user_id": userID,
			"incude_pineapples": includePineapples,
		})
	}
	// ...
}

func applesByUser(userID uuid.UUID, includePineapples bool) (int, error) {
	fruits, err := fruitsByUser(userID)
	if err != nil {
		return 0, meh.Wrap(err, "fruits by user", meh.Details{"user_id": userID})
	}
	// ...
}

func fruitsByUser(userID uuid.UUID) ([]Fruit, error) {
	fruitsRaw, err := readFruitsFile()
	if err != nil {
		return nil, meh.Wrap(err, "read fruits file", nil)
	}
	// ...
}

func readFruitsFile() ([]byte, error) {
	const fruitsFilename = "fruits.txt"
	b, err := os.ReadFile(fruitsFilename)
	if err != nil {
		return nil, meh.NewInternalErrFromErr(err, "read fruits file", meh.Details{
			"fruits_filename": fruitsFilename,
		})
	}
	return b, nil
}

By wrapping the error, the meh.ErrInternal-code from readFruitsFile is preserved. Details are passed as well and the final error message would look like this:

apples by user: fruits by user: read fruits file: file not found

Of course, the error code could also be changed. For example, if the returned error from os.ReadFile is checked to be a os.ErrNotExist and meh.ErrNotFound is returned, fruitsByUser could then return meh.NewInternalErrFromErr(err, ...) in order to change the code to ErrInternal as readFruitsFile is expected to not fail.

Checking the error code

As already mentioned, each layer of "wrapping" is represented another meh.Error with its own code. If you want to check the actual error code, use meh.ErrorCode(err error). This will return the error code of the first error without meh.ErrNeutral-code, which is set when wrapping errors.

Logging

Documentation

Support for logging with zap comes out of the box and is provided with the package mehlog.

Set the log-level translation with mehlog.SetDefaultLevelTranslator and log with mehlog.Log. This logs the error to the level which is determined by the error code (same as meh.ErrorCode).

HTTP support

Documentation

In combination with mehlog, support for responding with the correct status code and logging error details with request details is provided. Currently, features are rather limited and serve more as an example.

Set the status code mapping with mehhttp.SetHTTPStatusCodeMapping. You can then log and respond using mehhttp.LogAndRespondError. This logs the error along with request details and responds with the determined HTTP status code and an empty message.

The following additional error codes are provided:

  • mehhttp-communication: Used for all problems regarding client communication because communication is unstable by nature and not always an internal error.
  • mehhttp-service-not-reachable: Used for problems with requesting third-party services.

PostgreSQL support

Documentation

Support for errors of type pgconn.PgError is provided using these generators:

func NewQueryDBErr(err error, message string, query string) error
func NewScanRowsErr(err error, message string, query string) error 

NewQueryDBErr returns a meh.ErrBadInput-error if the error code has prefix 22 (data exception) or 23 (integrity constraint violation).

# Packages

Package mehgin provides some wrappers and utils for bridging mehhttp and gin.
Package mehhttp provides functionality for handling and responding errors.
Package mehlog allows logging of meh.Error to zap.Logger.
Package mehpg provides error functionality regarding postgres-errors.

# Functions

ApplyCode wraps the given error with the Code.
ApplyDetails wraps the given error with an ErrNeutral and the given Details.
ApplyStackTrace applies the current stack trace to the given error.
Cast tries to Cast the given error to *Error.
ClearPassThrough returns the given error with all pass-through-fields (from wrapped errors as well).
ErrorCode returns the first non ErrNeutral Code for the given error.
Finalize alters the given error for handing it off to other libraries.
NewBadInputErr creates a new ErrBadInput with the given message and details.
NewBadInputErrFromErr creates a new ErrBadInput with the given error to be wrapped, message and details.
NewErr creates a new Error with the given Code, message and details.
NewErrFromErr creates a new Error with the given wrapped one, Code, message and details.
NewErrorUnwrapper allows iterating the given error from top to bottom level using ErrorUnwrapper.Next in a loop and getting the current level's error using ErrorUnwrapper.Current.
NewForbiddenErr creates a new ErrForbidden with the given message and details.
NewForbiddenErrFromErr creates a new ErrForbidden with the given error to be wrapped, message and details.
NewInternalErr creates a new ErrInternal with the given message and details.
NewInternalErrFromErr creates a new ErrInternal with the given error to be wrapped, message and details.
NewNotFoundErr creates a new ErrNotFound with the given message and details.
NewNotFoundErrFromErr creates a new ErrNotFound with the given error to be wrapped, message and details.
NewPassThroughErr is similar to NewErrFromErr with the only difference that Error.WrappedErrPassThrough is set to true.
NewUnauthorizedErr creates a new ErrUnauthorized with the given message and details.
NewUnauthorizedErrFromErr creates a new ErrUnauthorized with the given error to be wrapped, message and details.
NilOrWrap returns nil if the given error is nil or calls meh.Wrap on it otherwise.
ToMap returns the details of the given error as a key-value map with appended enhanced information regarding the error itself (Error.Code to MapFieldErrorCode and the Error.Error-message to MapFieldErrorMessage).
Wrap wraps the given error with an ErrNeutral, the message and details.

# Constants

ErrBadInput is used when submitted data was invalid.
ErrForbidden is used for unauthorized access to resources.
ErrInternal is used for basic internal errors.
ErrNeutral is used mainly for wrapping in order to not change the Code.
ErrNotFound is used when requested resources where not found.
ErrUnauthorized is used for when the caller is not known but a resource required authorized access.
ErrUnexpected is the default error that is used when no other Code is specified.
Field names for usage in ToMap.
Field names for usage in ToMap.

# Structs

Error is the container for any relevant error information that needs to be kept when bubbling.
ErrorUnwrapper is used for iterating over the recursive structure of wrapped errors in Error.WrappedErr.
StackTrace holds an errors.StackTrace as well as a formatted stack trace for usage in logging.

# Type aliases

Code is the type of error in Error.
Details are optionally provided error details in Error.Details that are used for easier debugging and error locating.