DEV Community

Clavin June
Clavin June

Posted on • Originally published at clavinjune.dev on

My Custom Http Error In Golang

This is how I handle error with golang on http server

package e

import (
    "errors"
    "fmt"
    "net/http"
    "runtime"
)

// Error implements built in error with status code, caller, and message attribute
type Error struct {
    Err error
    statusCode int
    message string
    caller string
}

// New wraps err with defined statusCode and message
func New(err error, statusCode int, message string) error {
    return &Error{
        Err: err,
        statusCode: statusCode,
        message: message,
        caller: getCaller(),
    }
}

// Wrap wraps err with custom message
// Wrap's result inherit statusCode from err if err equals *Error
func Wrap(err error, msg string) error {
    var e *Error
    statusCode := http.StatusInternalServerError

    if errors.As(err, &e) {
        statusCode = e.statusCode
    }

    return &Error{
        Err: err,
        statusCode: statusCode,
        message: msg,
        caller: getCaller(),
    }
}

// From creates new error from defined statusCode
// if statusCode doesn't have any status text
// statusCode changed to http.StatusInternalServerError
func From(statusCode int) error {
    text := http.StatusText(statusCode)

    if text == "" {
        text = http.StatusText(http.StatusInternalServerError)
        statusCode = http.StatusInternalServerError
    }

    return &Error{
        Err: errors.New(text),
        statusCode: statusCode,
        message: "",
        caller: getCaller(),
    }
}

// Error returns error message with caller
func (e Error) Error() string {
    if e.message == "" {
        return e.Err.Error()
    }

    return fmt.Sprintf("%v\n[%v] > %v", e.Err, e.caller, e.message)
}

// Unwrap enables errors.As and errors.Is
func (e Error) Unwrap() error {
    return e.Err
}

// StatusCode returns e.statusCode
func (e Error) StatusCode() int {
    return e.statusCode
}

// getCaller uses log.Lshortfile to format the caller
func getCaller() string {
    _, file, line, ok := runtime.Caller(2)
    if !ok {
        file = "???"
        line = 0
    }

    short := file
    for i := len(file) - 1; i > 0; i-- {
        if file[i] == '/' {
            short = file[i+1:]
            break
        }
    }
    file = short

    return fmt.Sprintf("%s:%d", file, line)
}
Enter fullscreen mode Exit fullscreen mode

Usage:

package main

import (
    "database/sql"
    "errors"
    "fmt"

    "e"
)

func main() {
    base := e.New(sql.ErrNoRows, 404, "user not found")
    base2 := e.Wrap(base, "bleh")
    concrete := e.Wrap(base2, "there's no user with such credentials")

    fmt.Println(concrete)
    fmt.Println(errors.Is(concrete, base))

    var ee *Error
    if errors.As(concrete, &ee) {
        fmt.Println(ee.StatusCode())
    }
}
Enter fullscreen mode Exit fullscreen mode

Result:

sql: no rows in result set
[main.go:11] > user not found
[main.go:12] > bleh
[main.go:13] > there's no user with such credentials
true
404
Enter fullscreen mode Exit fullscreen mode

Top comments (0)