bnkamalesh/errors

View on GitHub
errors.go

Summary

Maintainability
A
2 hrs
Test Coverage
// Package errors helps in wrapping errors with custom type as well as a user friendly message. This is particularly useful when responding to APIs
package errors

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "runtime"
    "strconv"
    "strings"
)

type errType int

func (e errType) Int() int {
    return int(e)
}

// While adding a new Type, the respective helper functions should be added, also update the
// WriteHTTP method accordingly
const (
    // TypeInternal is error type for when there is an internal system error. e.g. Database errors
    TypeInternal errType = iota
    // TypeValidation is error type for when there is a validation error. e.g. invalid email address
    TypeValidation
    // TypeInputBody is error type for when an input data type error. e.g. invalid JSON
    TypeInputBody
    // TypeDuplicate is error type for when there's duplicate content
    TypeDuplicate
    // TypeUnauthenticated is error type when trying to access an authenticated API without authentication
    TypeUnauthenticated
    // TypeUnauthorized is error type for when there's an unauthorized access attempt
    TypeUnauthorized
    // TypeEmpty is error type for when an expected non-empty resource, is empty
    TypeEmpty
    // TypeNotFound is error type for an expected resource is not found e.g. user ID not found
    TypeNotFound
    // TypeMaximumAttempts is error type for attempting the same action more than allowed
    TypeMaximumAttempts
    // TypeSubscriptionExpired is error type for when a user's 'paid' account has expired
    TypeSubscriptionExpired
    // TypeDownstreamDependencyTimedout is error type for when a request to a downstream dependent service times out
    TypeDownstreamDependencyTimedout

    // DefaultMessage is the default user friendly message
    DefaultMessage = "unknown error occurred"
)

var (
    defaultErrType = TypeInternal
)

// Error is the struct which holds custom attributes
type Error struct {
    // original is the original error
    original error
    // Message is meant to be returned as response of API, so this should be a user-friendly message
    message string
    // Type is used to define the type of the error, e.g. Server error, validation error etc.
    eType errType
    pcs   []uintptr
    pc    uintptr
}

func (e *Error) fileLine() string {
    if e.pc == 0 {
        return ""
    }

    frames := runtime.CallersFrames([]uintptr{e.pc + 1})
    frame, _ := frames.Next()

    buff := bytes.NewBuffer(make([]byte, 0, 128))
    buff.WriteString(frame.File)
    buff.WriteString(":")
    buff.WriteString(strconv.Itoa(frame.Line))

    return buff.String()
}

// Error is the implementation of error interface
func (e *Error) Error() string {
    str := bytes.NewBuffer(make([]byte, 0, 128))
    str.WriteString(e.fileLine())
    if str.Len() != 0 {
        str.WriteString(": ")
    }

    if e.original != nil {
        str.WriteString(e.message)
        str.WriteString("\n")
        str.WriteString(e.original.Error())
        return str.String()
    }

    if e.message != "" {
        str.WriteString(e.message)
        return str.String()
    }

    str.WriteString(DefaultMessage)

    return str.String()
}

// ErrorWithoutFileLine prints the final string without the stack trace / file+line number
func (e *Error) ErrorWithoutFileLine() string {
    if e.original != nil {
        if e.message != "" {
            msg := bytes.NewBuffer(make([]byte, 0, 128))
            msg.WriteString(e.message)
            msg.WriteString(": ")
            if o, ok := e.original.(*Error); ok {
                msg.WriteString(o.ErrorWithoutFileLine())
            } else {
                msg.WriteString(e.original.Error())
            }
            return msg.String()
        }
        return e.original.Error()
    }

    if e.message != "" {
        return e.message
    }

    return e.fileLine()
}

// Message returns the user friendly message stored in the error struct. It will ignore all errors
// which are not of type *Error
func (e *Error) Message() string {
    messages := make([]string, 0, 5)
    if e.message != "" {
        messages = append(messages, e.message)
    }

    err, _ := e.original.(*Error)
    for err != nil {
        if err.message == "" {
            err, _ = err.original.(*Error)
            continue
        }
        messages = append(messages, err.message)
        err, _ = err.original.(*Error)
    }

    if len(messages) > 0 {
        return strings.Join(messages, ": ")
    }

    return e.Error()
}

// Unwrap implement's Go 1.13's Unwrap interface exposing the wrapped error
func (e *Error) Unwrap() error {
    return e.original
}

// Is implements the Is interface required by Go
func (e *Error) Is(err error) bool {
    o, _ := err.(*Error)
    return o == e
}

// HTTPStatusCode is a convenience method used to get the appropriate HTTP response status code for the respective error type
func (e *Error) HTTPStatusCode() int {
    status := http.StatusInternalServerError
    switch e.eType {
    case TypeValidation:
        {
            status = http.StatusUnprocessableEntity
        }
    case TypeInputBody:
        {
            status = http.StatusBadRequest
        }

    case TypeDuplicate:
        {
            status = http.StatusConflict
        }

    case TypeUnauthenticated:
        {
            status = http.StatusUnauthorized
        }
    case TypeUnauthorized:
        {
            status = http.StatusForbidden
        }

    case TypeEmpty:
        {
            status = http.StatusGone
        }

    case TypeNotFound:
        {
            status = http.StatusNotFound

        }
    case TypeMaximumAttempts:
        {
            status = http.StatusTooManyRequests
        }
    case TypeSubscriptionExpired:
        {
            status = http.StatusPaymentRequired
        }
    }

    return status
}

// Type returns the error type as integer
func (e *Error) Type() errType {
    return e.eType
}

// Format implements the verbs/directives supported by Error to be used in fmt annotated/formatted strings
/*
%v  - the same output as Message(). i.e. recursively get all the custom messages set by user
    - if any of the wrapped error is not of type *Error, that will *not* be displayed
%+v - recursively prints all the messages along with the file & line number. Also includes output of `Error()` of
non *Error types.

%s  - identical to %v
%+s - recursively prints all the messages without file & line number. Also includes output `Error()` of
non *Error types.
*/
func (e *Error) Format(s fmt.State, verb rune) {
    switch verb {
    case 'v':
        if s.Flag('+') {
            _, _ = io.WriteString(s, e.Error())
        } else {
            _, _ = io.WriteString(s, e.Message())
        }
    case 's':
        {
            if s.Flag('+') {
                _, _ = io.WriteString(s, e.ErrorWithoutFileLine())
            } else {
                _, _ = io.WriteString(s, e.Message())
            }
        }
    }
}

func (e *Error) RuntimeFrames() *runtime.Frames {
    return runtime.CallersFrames(e.ProgramCounters())
}

func (e *Error) ProgramCounters() []uintptr {
    return e.pcs
}

func (e *Error) StackTrace() []string {
    rframes := e.RuntimeFrames()
    frame, ok := rframes.Next()
    buff := bytes.NewBuffer(make([]byte, 0, 128))
    buff.WriteString(frame.Function)
    buff.WriteString("(): ")
    buff.WriteString(e.message)

    trace := make([]string, 0, len(e.ProgramCounters()))
    trace = append(trace, buff.String())
    for ok {
        buff.Reset()
        buff.WriteString("\t")
        buff.WriteString(frame.File)
        buff.WriteString(":")
        buff.WriteString(strconv.Itoa(frame.Line))
        trace = append(trace, buff.String())
        frame, ok = rframes.Next()
    }
    return trace
}

func (e *Error) StackTraceNoFormat() []string {
    rframes := e.RuntimeFrames()
    frame, ok := rframes.Next()
    line := strconv.Itoa(frame.Line)

    buff := bytes.NewBuffer(make([]byte, 0, 128))
    buff.WriteString(frame.Function)
    buff.WriteString("(): ")
    buff.WriteString(e.message)

    trace := make([]string, 0, len(e.ProgramCounters()))
    trace = append(trace, buff.String())
    for ok {
        buff.Reset()
        buff.WriteString(frame.File)
        buff.WriteString(":")
        buff.WriteString(line)
        trace = append(trace, buff.String())
        frame, ok = rframes.Next()
    }
    return trace
}

// StackTraceCustomFormat lets you prepare a stacktrace in a custom format
/*
Supported directives:
%m - message
%p - file path
%l - line
%f - function
*/
func (e *Error) StackTraceCustomFormat(msgformat string, traceFormat string) []string {
    rframes := e.RuntimeFrames()
    frame, ok := rframes.Next()

    message := strings.ReplaceAll(msgformat, "%m", e.message)
    message = strings.ReplaceAll(message, "%p", frame.File)
    message = strings.ReplaceAll(message, "%l", strconv.Itoa(frame.Line))
    message = strings.ReplaceAll(message, "%f", frame.Function)
    traces := make([]string, 0, len(e.ProgramCounters()))
    traces = append(traces, message)

    for ok {
        trace := strings.ReplaceAll(traceFormat, "%m", e.message)
        trace = strings.ReplaceAll(trace, "%p", frame.File)
        trace = strings.ReplaceAll(trace, "%l", strconv.Itoa(frame.Line))
        trace = strings.ReplaceAll(trace, "%f", frame.Function)

        traces = append(traces, trace)
        frame, ok = rframes.Next()
    }

    return traces
}

// New returns a new instance of Error with the relavant fields initialized
func New(msg string) *Error {
    return newerr(nil, msg, defaultErrType, 3)
}

func Newf(fromat string, args ...interface{}) *Error {
    return newerrf(nil, defaultErrType, 4, fromat, args...)
}

// Errorf is a convenience method to create a new instance of Error with formatted message
// Important: %w directive is not supported, use fmt.Errorf if you're using the %w directive or
// use Wrap/Wrapf to wrap an error.
func Errorf(fromat string, args ...interface{}) *Error {
    return newerrf(nil, defaultErrType, 4, fromat, args...)
}

// SetDefaultType will set the default error type, which is used in the 'New' function
func SetDefaultType(e errType) {
    defaultErrType = e
}

// Stacktrace returns a string representation of the stacktrace, where each trace is separated by a newline and tab '\t'
func Stacktrace(err error) string {
    trace := make([][]string, 0, 128)
    for err != nil {
        e, ok := err.(*Error)
        if ok {
            trace = append(trace, e.StackTrace())
        } else {
            trace = append(trace, []string{err.Error()})
        }
        err = Unwrap(err)
    }

    lookup := map[string]struct{}{}
    for idx := len(trace) - 1; idx >= 0; idx-- {
        list := trace[idx]
        uniqueList := make([]string, 0, len(list))
        for _, line := range list {
            _, ok := lookup[line]
            if ok {
                break
            }
            uniqueList = append(uniqueList, line)
            lookup[line] = struct{}{}
        }
        trace[idx] = uniqueList
    }
    final := make([]string, 0, len(trace)*3)
    for _, list := range trace {
        final = append(final, list...)
    }

    return strings.Join(final, "\n")
}

// Stacktrace returns a string representation of the stacktrace, as a slice of string where each
// element represents the error message and traces.
func StacktraceNoFormat(err error) []string {
    trace := make([][]string, 0, 128)
    for err != nil {
        e, ok := err.(*Error)
        if ok {
            trace = append(trace, e.StackTraceNoFormat())
        } else {
            trace = append(trace, []string{err.Error()})
        }
        err = Unwrap(err)
    }

    lookup := map[string]struct{}{}
    for idx := len(trace) - 1; idx >= 0; idx-- {
        list := trace[idx]
        uniqueList := make([]string, 0, len(list))
        for _, line := range list {
            _, ok := lookup[line]
            if ok {
                break
            }
            uniqueList = append(uniqueList, line)
            lookup[line] = struct{}{}
        }
        trace[idx] = uniqueList
    }
    final := make([]string, 0, len(trace)*3)
    for _, list := range trace {
        final = append(final, list...)
    }

    return final
}

// StacktraceCustomFormat lets you prepare a stacktrace in a custom format
/*
msgformat - is used to format the line which prints message from Error.message
traceFormat - is used to format the line which prints trace
Supported directives:
%m - message if err type is *Error, otherwise output of `.Error()`
%p - file path, empty if type is not *Error
%l - line, empty if type is not *Error
%f - function, empty if type is not *Error
*/
func StacktraceCustomFormat(msgformat string, traceFormat string, err error) string {
    trace := make([][]string, 0, 128)
    for err != nil {
        e, ok := err.(*Error)
        if ok {
            trace = append(trace, e.StackTraceCustomFormat(msgformat, traceFormat))
        } else {
            message := strings.ReplaceAll(msgformat, "%m", err.Error())
            message = strings.ReplaceAll(message, "%p", "")
            message = strings.ReplaceAll(message, "%l", "")
            message = strings.ReplaceAll(message, "%f", "")
            trace = append(trace, []string{message})
        }
        err = Unwrap(err)
    }

    lookup := map[string]struct{}{}
    for idx := len(trace) - 1; idx >= 0; idx-- {
        list := trace[idx]
        uniqueList := make([]string, 0, len(list))
        for _, line := range list {
            _, ok := lookup[line]
            if ok {
                break
            }
            uniqueList = append(uniqueList, line)
            lookup[line] = struct{}{}
        }
        trace[idx] = uniqueList
    }

    final := make([]string, 0, len(trace)*3)
    for _, list := range trace {
        final = append(final, list...)
    }

    return strings.Join(final, "")
}

func ProgramCounters(err error) []uintptr {
    pcs := make([][]uintptr, 0, 128)
    for err != nil {
        e, ok := err.(*Error)
        if ok {
            pcs = append(pcs, e.ProgramCounters())
        }
        err = Unwrap(err)
    }

    lookup := map[uintptr]struct{}{}
    for idx := len(pcs) - 1; idx >= 0; idx-- {
        list := pcs[idx]
        uniqueList := make([]uintptr, 0, len(list))
        for _, line := range list {
            _, ok := lookup[line]
            if ok {
                break
            }
            uniqueList = append(uniqueList, line)
            lookup[line] = struct{}{}
        }
        pcs[idx] = uniqueList
    }
    final := make([]uintptr, 0, len(pcs)*3)
    for _, list := range pcs {
        final = append(final, list...)
    }
    return final
}

func RuntimeFrames(err error) *runtime.Frames {
    pcs := ProgramCounters(err)
    return runtime.CallersFrames(pcs)
}

func StacktraceFromPcs(err error) string {
    pcs := ProgramCounters(err)
    frames := runtime.CallersFrames(pcs)
    frame, hasMore := frames.Next()
    lines := make([]string, 0, len(pcs))
    if !hasMore {
        lines = append(lines, frame.File+":"+strconv.Itoa(frame.Line)+":"+frame.Function+"()")
    }
    for hasMore {
        lines = append(lines, frame.File+":"+strconv.Itoa(frame.Line)+":"+frame.Function+"()")
        frame, hasMore = frames.Next()
    }

    return strings.Join(lines, "\n")
}