- Provides AppError type to be used for wrapping any kind of errors in an application
- Supports a centralized definition of errors
- AppError can carry extra information such as
cause
,debug log
, and stack trace - AppError supports translating its error message into a specific language
- AppError can be transformed to a JSON structure which is friendly to client side
- Provides MultiError type to handle multiple AppError(s) with a common use case of validation errors
go get github.com/tiendc/go-apperrors
Initializes go-apperrors
at program startup
import gae "github.com/tiendc/go-apperrors"
func main() {
...
gae.Init(&gae.Config{
Debug: ENV == "development",
DefaultLogLevel: gae.LogLevelInfo,
TranslationFunc: func (lang gae.Language, key string, params map[string]any) {
// Provides your implementation to translate message
},
})
...
}
Defines your errors
// It is recommended to add a new directory for placing app errors.
// In this example, I use `apperrors/errors.go`.
import gae "github.com/tiendc/go-apperrors"
// Some base errors
var (
ErrInternalServer = gae.Create("ErrInternalServer", &gae.ErrorConfig{
Status: http.StatusInternalServerError,
LogLevel: gae.LogLevelError, // this indicates an unexpected error
})
ErrUnauthorized = gae.Create("ErrUnauthorized", &gae.ErrorConfig{Status: http.StatusUnauthorized})
ErrNotFound = gae.Create("ErrNotFound", &gae.ErrorConfig{Status: http.StatusNotFound})
...
)
// Errors from external libs
var (
ErrRedisKeyNotFound = gae.Add(redis.Nil, &gae.ErrorConfig{Status: http.StatusNotFound})
)
// Some more detailed errors
var (
ErrUserNotInProject = gae.Create("ErrUserNotInProject", &gae.ErrorConfig{Status: http.StatusForbidden})
...
)
Handles errors in your main processing code
// There are some use cases as below.
// 1. You get an unexpected error
// Just wrap it and return. This will result in error 500 returned to client.
resp, err := updateProject(project)
if err != nil {
return gae.New(err).WithDebug("extra info: project %s", project.ID)
// OR `return gae.Wrap(err)` if you don't need to add extra info
}
// 2. You get an error which may be expected
resp, err := deleteProject(project)
if err != nil {
if errors.Is(err, DBNotFound) { // this error can be expected
return gae.New(gae.ErrNotFound).WithCause(err)
}
return gae.Wrap(err) // unexpected error
}
// 3. You want to return an error when a condition isn't satisfied
if `user.ID` is not in `project.userIDs` {
// This will return error Forbidden to client as we configured previously
return gae.New(gae.ErrUserNotInProject)
}
Handles validation errors
// Validation is normally performed when you parse requests from client.
// You may use an external lib for the validation. That's why you need to make
// `adapter` code to transform the validation errors to `AppError`s.
// Add a new file to the above directory, says `apperrors/validation_errors.go`.
// This building function will be used later to create validation error.
func ValidationErrorInfoBuilder(err AppError, buildCfg *gae.InfoBuilderConfig) *gae.InfoBuilderResult {
// Extracts the inner error and casts it to the validation error type you use
vldErr := &thirdPartyLib.Error{}
if !errors.As(err, &vldErr) {
// panic, this should not happen
}
return &gae.InfoBuilderResult{
// Transform the error from the 3rd party lib to ErrorInfo struct
ErrorInfo: &gae.ErrorInfo{
Message: buildCfg.TranslationFunc(buildCfg.Language, vldErr.getMessage(), err.Params()),
Source: vldErr.getSource(),
...
}
}
}
// When parse your request
func (req UpdateProjectReq) Validate() gae.ValidationError {
vldErrors := validateReq(req)
return gae.NewValidationErrorWithInfoBuilder(apperrors.ValidationErrorInfoBuilder, vldErrors...)
}
Handles errors before returning them to client
// In the base handler, implements function `RenderError()`
func RenderError(err error) {
// Gets language from request, you can use `gae.ParseAcceptLanguage()`
lang := parseLanguageFromRequest()
// Call goapperrors.Build
buildResult := gae.Build(err, lang)
// Logs error to Sentry or a similar service
if buildResult.ErrorInfo.LogLevel != gae.LogLevelNone {
logErrorToSentry(err, buildResult.ErrorInfo.LogLevel)
}
// Sends the error as JSON to client
response.SendJSON(buildResult.ErrorInfo)
}
// In your specific handler, for instance, project handler
func (h ProjectHandler) UpdateProject() {
updateProjectReq, err := parseAndValidateRequest(httpReq)
if err != nil {
baseHandler.RenderError(err)
return
}
resp, err := useCase.UpdateProject(updateProjectReq)
if err != nil {
baseHandler.RenderError(err)
return
}
// Send result to client
response.SendJSON(resp)
}
You should synchronize this option with the ENV value. If it is true
, error building will
return the fields Cause
and Debug
which is convenient for development. If it is false
,
they will not be returned which is more secured in production
env as their values can
have sensitive information.
If this value is default (which is nil
), go-apperrors
will use the lib
github.com/go-errors/errors to wrap and attach stack trace to errors.
If you don't want to attach stack trace, provide a self-implementation on initialization.
Init(&Config{
WrapFunc: func (err error) error { return err }
})
Sets this option by providing a function to help the lib translate error messages. Otherwise, the translation will be disabled.
Init(&Config{
TranslationFunc: func (lang Language, key string, params map[string]any) {
// Provides your implementation to translate message
},
})
When translation fails, if this flag is true
, the error content will be used to assign to
the output Message
field. Otherwise, the output message will be empty.
NOTE: turn off this flag if you don't want to reveal sensitive information on building.
- You are welcome to make pull requests for new functions and bug fixes.