Skip to content

failure is a utility package for handling application errors.

License

Notifications You must be signed in to change notification settings

morikuni/failure

Repository files navigation

failure

Go Reference

Package failure is an error handling library for Go. It allows you to create, wrap, and handle errors with additional context and features.

Features

  • Create errors with error codes to easily classify and handle errors.
  • Wrap errors with additional context such as function parameters and key-value data.
  • Automatically capture call stack information for debugging.
  • Flexible error formatting for both developers and end users.
  • Utility functions to extract error codes, messages, and other metadata from errors.

Installation

To install failure, use the following command:

go get github.com/morikuni/failure/v2

Usage Examples

First, define your application's error codes:

type ErrorCode string

const (
    ErrNotFound ErrorCode = "NotFound"
    ErrInvalidArgument ErrorCode = "InvalidArgument"
)

Use failure.New to create a new error with an error code:

err := failure.New(ErrNotFound, failure.Message("Resource not found"))

Use failure.Wrap to wrap an existing error with additional context:

if err != nil {
    return failure.Wrap(err, failure.Context{"parameter": "value"})
}

Use failure.Is to check for a specific error code and handle the error:

if failure.Is(err, ErrNotFound) {
    // Handle ErrNotFound error
}

Use utility functions to extract metadata from the error:

code := failure.CodeOf(err)
message := failure.MessageOf(err)
callStack := failure.CallStackOf(err)

Example error outputs:

err := failure.New(ErrInvalidArgument, failure.Message("Invalid argument"), failure.Context{"userId": "123"})
fmt.Println(err) 
// Output: GetUser[InvalidArgument](Invalid argument, {userId=123})

fmt.Printf("%+v\n", err)
// Output:
// [main.GetUser] /path/to/file.go:123
//     InvalidArgument
//     Invalid argument
//     {userId=123}
// [CallStack]
//     [main.GetUser] /path/to/file.go:123
//     [main.main] /path/to/main.go:456

For more detailed usage and examples, refer to the Go Reference.

Full Example of Usage

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/http/httputil"

	"github.com/morikuni/failure/v2"
)

type ErrorCode string

const (
	NotFound  ErrorCode = "NotFound"
	Forbidden ErrorCode = "Forbidden"
)

func GetACL(projectID, userID string) (acl interface{}, e error) {
	notFound := true
	if notFound {
		return nil, failure.New(NotFound,
			failure.Context{"project_id": projectID, "user_id": userID},
		)
	}
	return nil, failure.Unexpected("unexpected error")
}

func GetProject(projectID, userID string) (project interface{}, e error) {
	_, err := GetACL(projectID, userID)
	if err != nil {
		if failure.Is(err, NotFound) {
			return nil, failure.Translate(err, Forbidden,
				failure.Message("no acl exists"),
				failure.Context{"additional_info": "hello"},
			)
		}
		return nil, failure.Wrap(err)
	}
	return nil, nil
}

func Handler(w http.ResponseWriter, r *http.Request) {
	_, err := GetProject(r.FormValue("project_id"), r.FormValue("user_id"))
	if err != nil {
		HandleError(w, err)
		return
	}
}

func getHTTPStatus(err error) int {
	switch failure.CodeOf(err) {
	case NotFound:
		return http.StatusNotFound
	case Forbidden:
		return http.StatusForbidden
	default:
		return http.StatusInternalServerError
	}
}

func getMessage(err error) string {
	msg := failure.MessageOf(err)
	if msg != "" {
		return string(msg)
	}
	return "Error"
}

func HandleError(w http.ResponseWriter, err error) {
	w.WriteHeader(getHTTPStatus(err))
	io.WriteString(w, getMessage(err))

	fmt.Println("============ Error ============")
	fmt.Printf("Error = %v\n", err)
	// Error = main.GetProject[Forbidden](no acl exists, {additional_info=hello}): main.GetACL[NotFound]({project_id=aaa,user_id=111})

	code := failure.CodeOf(err)
	fmt.Printf("Code = %v\n", code)
	// Code = Forbidden

	msg := failure.MessageOf(err)
	fmt.Printf("Message = %v\n", msg)
	// Message = no acl exists

	cs := failure.CallStackOf(err)
	fmt.Printf("CallStack = %v\n", cs)
	// CallStack = main.GetACL: main.GetProject: main.Handler: main.main: runtime.main: goexit

	fmt.Printf("Cause = %v\n", failure.CauseOf(err))
	// Cause = main.GetACL[NotFound]({project_id=aaa,user_id=111})

	fmt.Println()
	fmt.Println("============ Detail ============")
	fmt.Printf("%+v\n", err)
	// [main.GetProject] /go/src/github.com/morikuni/failure/example/main.go:34
	//     Forbidden
	//     no acl exists
	//     {additional_info=hello}
	// [main.GetACL] /go/src/github.com/morikuni/failure/example/main.go:23
	//     NotFound
	//     {user_id=111,project_id=aaa}
	// [CallStack]
	//     [main.GetACL] /go/src/github.com/morikuni/failure/example/main.go:23
	//     [main.GetProject] /go/src/github.com/morikuni/failure/example/main.go:31
	//     [main.Handler] /go/src/github.com/morikuni/failure/example/main.go:45
	//     [main.main] /go/src/github.com/morikuni/failure/example/main.go:119
	//     [runtime.main] /opt/homebrew/opt/go/libexec/src/runtime/proc.go:271
	//     [runtime.goexit] /opt/homebrew/opt/go/libexec/src/runtime/asm_arm64.s:1222	
}

func main() {
	req := httptest.NewRequest(http.MethodGet, "/?project_id=aaa&user_id=111", nil)
	rec := httptest.NewRecorder()
	Handler(rec, req)

	res, _ := httputil.DumpResponse(rec.Result(), true)
	fmt.Println("============ Dump ============")
	fmt.Println(string(res))
}

Migration from v1 to v2

See docs/v1-to-v2.md for migration guide.

Contributing

Contributions are welcome! Feel free to send issues or pull requests.

License

This project is licensed under the MIT License. See the LICENSE file for details.