My Custom HTTP Error in Golang

tech · May 18, 2021 · ~3 min

This is how I handle error with golang on http server

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
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)
}

Usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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())
  }
}

Result:

1
2
3
4
5
6
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

Thank you for reading!

· · ·

Love This Content?

Any kind of supports is greatly appreciated! Kindly support me via Bitcoin, Ko-fi, Trakteer, or just continue to read another content. You can write a response via Webmention and let me know the URL via Telegraph.