Error Handling

Introduction

Error handling is very different in Go than in other languages. With Go the following applies: The more time you spend on errors, the faster bugs can be found. It therefore deserves its own chapter.

Panic And Return Errors

In other languages you throw an exception (or error). If the caller wants to do something based on that error, then you have to catch the error. This is very different with Go. The error is passed on until you can do something with it. If you want to stop the process and just fire the error, you can use panic.

Return Errors

The most common way is to return the error from the function:

import "github.com/confetti-framework/errors"

var NoUserFound = errors.New("no user found")

func GetUser() (model.User, error) {
    //

    if (user == nil) {
        return users.NewUnregistredUser(), NoUserFound
    }

    return user, nil
}

The following example shows how the caller can handle the error.

user, err := GetUser()
if err == NoUserFound {
    //
}

Ignore Errors

If you want to use the default user when the error occurs, you could ignore the error by an underscore:

user, _ := GetUser()

Wrap

By applying multiple layers, you can add more information to the error. You can use the Wrap method to prefix a message (with validation error: no user found as a result).

user, err := GetUser()
err.Wrap("validation error")

Unwrap

To receive the original error (after Wrap), you can use Unwrap (with no user found as a result):

err := errors.New("no user found").Wrap("validation error")
err.Unwrap().Error()

Apply Stack Trace

If you have a standard error, it does not contain a stack trace. Use function Wrap or WithStack To put the trace on it:

errors.Wrap(err, "can't connect to database")
errors.WithStack(err)

Log Level

The default log level is Emergency. To determine the log level you can use the Level method:

errros.New("username not found").Level(log_level.INFO)

HTTP Status

The default HTTP status is 500 Internal Server Error. To determine the response status you can use the Status method:

err := errros.New("username not found").Status(http.StatusNotFound)
return outcome.Html(err)

Custom

Do you want to add extra data to an error? In other languages you would extend a class. Go has a SOLID solution for this: Each error can be wrapped in multiple structs. To add data to an error you just have to create a wrapper yourself (which then also contains the original error). If you want to add an error code to your error, you can make te following:

func WithCode(err error, code string) *withCode {
    if err == nil {
        return nil
    }
    return &withCode{
        err,
        code,
    }
}

type withCode struct {
    cause error
    code string
}

func (w *withCode) Error() string {
    return w.cause.Error() + " with code " + w.code
}

func (w *withCode) Unwrap() error {
    return w.cause
}

func (w *withCode) Code() string {
    return w.code
}

Then the error can build up like this:

WithCode(errros.New("username not found"), "external_error")

In method Error() above, we put 'code' behind the message. But if you want to adjust the response, you can determine this in ResponseServiceProvider.

Panic

In case of a server error where the request cannot proceed, you could choose to use panic:

func GetUser() (model.User) {
    con, err := db.Connection()
    if (err != nil) {
        panic(err)
    }

    //
}

Confetti automatically ensures that the correct http response is generated.

Using panic can save you a lot of time. However, if you want to build a robust application, use panic only for critical or unexpected errors.

Message Convention

As you can see above, you can supplement the error with more information. Therefore, it is a convention to use a lowercase letter at the beginning of an error. Also, a dot at the end of the sentence can cause that the sentence can't be made longer. The errors are eventually automatically capitalized at the beginning of the sense.

Helpers

Is

An error can be made up of several layers with structs. If you want to know if a certain struct is present, you can use the Is helper. In the running example, validateUser() returns a validationError error:

var noUserFound = New("no user found")
var validationError = Wrap(noUserFound, "validation error")

err := validateUser()
if errors.Is(err, noUserFound) {
    // validationError contains noUserFound error
}

As

If you want to retrieve a specific struct, you can use the As helper. Before calling As, you have to define what needs to be searched and filled (which may be a struct or an interface).

func FindCode(err error) (string, bool) {
    var code string
    var codeHolder *withCode

    if !As(err, &codeHolder) {
        return "unkown code", false
    }

    return codeHolder.code, true
}

If you call As, a bool is returned on which you can check whether it was successful.

Configuration

Defining Errors

For the sake of simplicity, you have seen examples where we place the errors above the functions. It would be better to have an overview of all errors that can occur in the system. You can define your errors in app/report/errors.go:

var UserNotFound = errors.New("user not found").Status(net.StatusBadRequest
var Unauthorized = UserError.Status(net.StatusUnauthorized)

Global Log Context

If you want to add information to all errors, you can append that in app/report/errors.go. In the following example you can see that we apply Status and log Level globally:

var UserError = errors.New("").Status(net.StatusBadRequest).Level(log_level.INFO)
var Unauthorized = UserError.Status(net.StatusUnauthorized)
var SessionInvalid = Unauthorized.Wrap("session is not valid")
var SessionExpired = Unauthorized.Wrap("session expired")

Information Provision

The Debug option in your config/app.go configuration file determines how much information about an error is actually displayed to the user. By default, this option is set to respect the value of the APP_DEBUG environment variable, which is stored in your .env file.

For local development, you should set the APP_DEBUG environment variable to true. In your production environment, this value should always be false. If the value is set to true in production, you risk exposing sensitive configuration values to your application's end users.

Ignoring Errors By Type

The NoLogging field in config/errors.go contains a slice of errors that will not be logged. For example, errors resulting from 404 errors, as well as several other types of errors, are not written to your log files. You may add other error types to this array as needed:

NoLogging: []error{
    report.ValidationError,
    report.NotFoundError,
},

Custom HTTP Error Pages

Confetti makes it easy to display custom error pages. You can edit template resources/views/error.gohtml design your own error page. The following variables can be used when using this template:

{{- /*gotype: github.com/confetti-framework/foundation/encoder.ErrorView*/ -}}
<html lang="{{.Locale}}">
<h1>{{.AppName}}</h1>
<h2>{{.Status}} | {{.Message}}</h2>
<p>{{.StackTrace}}</p>

To add your own variables, you can edit the view placed in resources/views/error.go. Do you want to have even more control over how you convert errors to html? Than you can replace the encoder.ErrorToHtml in ResponseServiceProvider with your own encoder.

Contributors: Reindert Vetter, Vaggelis Yfantis