A simple helper package for errors in go.
While the project is in v0.0.0 the api is not guaranteed.
Logging is where the simple wrap information is useful. Wrap information should never be used in interface values. If you want a specific message to surface to the interface, see the WithMessage example.
typeErrorResponsestruct {
errorstringmessagestring
}
funcjsonResponseError(w http.ResponseWriter, errerror) {
logger.Error().LogError(err, "response")
switchtrue {
caseerros.Is(err,errors.NotFound):
w.WriteHeader(http.StatusOK)
w.Write(json.Unmarshall(err))
default:
w.WriteHeader(http.StatusInternalServerError)
w.Write(ErrorResponse{err.Error(), ""})
}
}
func (c*controller) Get(w http.ResponseWriter, r*http.Request) {
id:=getURLParam(r, "id")
it, err:=s.GetIt(id)
iferr!=nil {
jsonResponseError(w, err)
}
jsonResponse(w, it)
} func(s*service) GetIt(idstring) (It, error) {
it, err:=r.GetIt(id)
iferr!=nil {
returnIt{}, errors.Wrap(err, "service getting it")
}
}
func (r*repo) GetIt(idstring) (It, error) {
qry:="SELECT * FROM it WHERE id = $1"varitItiferr:=db.QueryRow(qry,id).scan(&it); err!=nil {
iferrors.Is(sql.ErrNoRows, err) {
returnIt{}, errors.Wrap(err, "repo getting it")
}
}WithMessage should be used when we have errors but we want to surface the error with a specific message without using a custom error. The highest in the chain WithMessage is used by default in this example.
In this example, we set a message that a record for the given id is not found. We really should, in this example, use WithError(err, errors.NotFound) to indicate that the record is not found, but the example is contrived to show how we can use the Message Value as an interface value.
typeErrorResponsestruct {
errstringmessagestring
}
funcjsonResponseError(w http.ResponseWriter, errerror) {
varmsgstringvarmessage errors.Messageiferrors.As(err, &message) {
msg=message.Value
}
logger.Error().With("msg", msg).LogError(err)
switchtrue {
caseerros.Is(err,errors.NotFound):
w.WriteHeader(http.StatusNotFound)
w.Write(ErrorResponse(errors.NotFound.Error(), msg))
default:
w.WriteHeader(http.StatusInternalServerError)
w.Write(ErrorResponse{"unhandled internal error", msg})
}
}
func (c*controller) Get(w http.ResponseWriter, r*http.Request) {
id:=getURLParam(r, "id")
it, err:=s.GetIt(id)
iferr!=nil {
jsonResponseError(w, err)
}
jsonResponse(w, it)
} func(s*service) GetIt(idstring) (It, error) {
it, err:=r.GetIt(id)
iferr!=nil {
returnIt{}, errors.Wrap(err, "service getting it")
}
}
func (r*repo) GetIt(idstring) (It, error) {
qry:="SELECT * FROM it WHERE id = $1"varitItiferr:=db.QueryRow(qry,id).scan(&it); err!=nil {
iferrors.Is(sql.ErrNoRows, err) {
returnIt{}, errors.WithMessage(err, fmt.Sprintf("no record for %q exists", id))
}
}WithError allows us to combine an error with another error.
Simple:
In this simple example, we us a sentinel error defined in the errors package to indicate that the error is not found. If you need more sentinel errors, feel free to define your own internal errors package and use those.
typeErrorResponsestruct {
errstringmessagestring
}
funcjsonResponseError(w http.ResponseWriter, errerror) {
varmsgstringvarmessage errors.Messageiferrors.As(err, &message) {
msg=message.Value
}
logger.Error().With("msg", msg).LogError(err)
switchtrue {
caseerros.Is(err,errors.NotFound):
w.WriteHeader(http.StatusNotFound)
w.Write(ErrorResponse(errors.NotFound.Error(), msg))
default:
}
}
func (c*controller) Get(w http.ResponseWriter, r*http.Request) {
id:=getURLParam(r, "id")
it, err:=s.GetIt(id)
iferr!=nil {
jsonResponseError(w, err)
}
jsonResponse(w, it)
} func(s*service) GetIt(idstring) (It, error) {
it, err:=r.GetIt(id)
iferr!=nil {
returnIt{}, errors.Wrap(err, "service getting it")
}
}
func (r*repo) GetIt(idstring) (It, error) {
qry:="SELECT * FROM it WHERE id = $1"varitItiferr:=db.QueryRow(qry,id).scan(&it); err!=nil {
iferrors.Is(sql.ErrNoRows, err) {
returnIt{}, errors.With(err, errors.NotFound)
}
}Custom Error Example
We often may need more than simple wrapped errors, error messages, or sentinel errors. In these scenarios we can rely on custom errors and combine them alongside with other errors like in the example below. With these custom errors, we can have more options in our handling of the error.
typeErrorResponsestruct {
errstringmessagestring
}
funcjsonResponseError(w http.ResponseWriter, errerror) {
varmsgstringvarmessage errors.Messageiferrors.As(err, &message) {
msg=message.Value
}
logger.Error().With("msg", msg).LogError(err)
varclerrCustomListErrorswitchtrue {
caseerros.As(err, &clerr):
w.WriteHeader(http.StatusInternalServerError)
ifclerr.Count>1 {
w.Write(ErrorResponse(clerr.Error(), msg))
}
ifclerr.Count==0 {
w.Write(ErrorResponse(clerr.Error(), msg))
}
fallthroughdefault:
w.WriteHeader(http.StatusInternalServerError)
w.Write(ErrorResponse{"not handled", msg})
}
}
func (c*controller) List(w http.ResponseWriter, r*http.Request) {
id:=getURLParam(r, "id")
it, err:=s.GetEm(id)
iferr!=nil {
jsonResponseError(w, err)
}
jsonResponse(w, it)
} func(s*service) List() ([]It, error) {
it, err:=r.List()
iferr!=nil {
returnnil, errors.Wrap(err, "service getting it")
}
}
typeCustomListErrorstruct {
CountstringQuerystring
}
func (cleCustomListError) Error() string {
returnfmt.Sprintf("expecting 1 records for query %q but found %q", cle.Count, cle.Query)
}
func (r*repo) List() ([]It, error) {
qry:="SELECT * FROM it"rows, err:=db.Query(qry)
varem []Itforrows.Next() {
varitIterr:=rows.Scan(&it)
}
iflen!=1 {
returnnil, CustomListError{
Count: len(em),
Query: qry,
}
}
...
}