Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to determine what kind of error message I should return from golang API?

Tags:

rest

go

I have a GoLang API with an SPA to consume it. What I do to errors in my API is return them until the handler where I test if an error from previous functions exist. If there is an error, I put it inside the response body, set status code to either 400 or 500 then return the response

in the handler function, to be able to create a clear message to the client side, I need to know what kind of error was returned, how do I do it?

I know about error types but I read about Dave Cheney's recommendation to just return an error along with a message (or wrap them in other words).

But if there are so many kinds of errors which might occur in the API call, then does it mean before returning the response, I need to check them all just to know what message I should return?

like image 876
JohnStephen.19 Avatar asked Jan 28 '23 22:01

JohnStephen.19


1 Answers

The first thing to say about errors is that just because there's an error interface

type error interface {
    Error() string
}

Does not mean that the error returned from any given method can only have that method / information attached to it.

One common method is to define your own error interface:

type myError interface {
    error // embeds the standard error interface
    OtherMethod() string // can define own methods here
}

When writing methods and functions it's really important to return an error and not myError, else you couple that method to your error implementation and cause dependency nightmares for yourself later.

Now that we've decided we can return extra information from error, using our own error interfaces you've got 3 main choices.

  1. Sentinel errors
  2. Error Failure types
  3. Errors with Behaviour

Sentinel errors

Sentinel errors are error values that are defined as package level variables, are exported and allow comparison to check for errors.

package myPackage

var ErrConnectionFailed = errors.New("connection failed")

func Connect() error {
    // trimmed ...
    return ErrConnectionFailed
}

A consumer of this example could use the connect function:

if err := myPackage.Connect(); err == myPackage.ErrConnectionFailed {
    // handle connection failed state
}

You can do a comparison to check if the error returned is equal to the sentinel error of the package. The drawback is that any error created with errors.New("connection failed") will be equal, and not just the error from myPackage.

Error failure types

Slightly better than sentinel errors are error failure types. We've already seen that you can define your own error interface, and if we say ours is now:

type MyError interface {
    error
    Failure() string
} 

type Err struct {
    failure string
}

func (e *Err) Error() string {
    // implement standard error
}

func (e *Err) Failure() string {
    return e.failure
}

const ConnFailed = "connection failed"

err := &Err{failure: ConnFailed}

In the consumer code you can get an error, check if it implements MyError and then do things with it.

err := myPackage.Connect()

if myErr, ok := err.(myPackage.MyError); ok {
    // here you know err is a MyError
    if myErr.Failure() == myPackage.ConnFailed {
        // handle connection failed, could also use a switch instead of if
    }
}

Now you have an idea of what caused the error which is nice. But do you really care what the cause was? Or do you only really care what you want to do to handle that error.

This is where errors with behaviour are nice.

Errors with behaviour

This is similar to defining your own error type, but instead you define methods that report information about that error. Given the example above, do you really care that the connection failed, or do you really only care if you can retry or if you need to error up the call stack again?

package myPackage

// this interface could report if the error
// is temporary and if you could retry it
type tempErr interface {
    Temporary() bool
}

func (e *Err) Temporary() bool {
    // return if the error is temporary or not
}

Now in the consumer (note you don't need to use the myPackage.tempErr), you can test using type assertions if the error is temporary and handle the retry case:

err := myPackage.Connect()

if tmp, ok := err.(interface { Temporary() bool }); ok && tmp.Temporary() {
    // the error is temporary and you can retry the connection
}

To answer the question, it's very hard to say without the specifics of the service that you are trying to implement. But as broad advice, I would try and use the last of the 3 examples as much as possible.

If the consumer of your service sends you some input that's not valid:

err := doThing(...)

if inv, ok := err.(interface { Invalid() bool }); ok && inv.Invalid() {
    // input is invalid, return 400 bad request status code etc.
}

If you want to return a specific message to a consumer, you could make that a method of your error type. Warning: this would give your packages knowledge that they are being used in a web service, etc.

err := doThing(...)

if msg, ok := err.(interface { ResponseMsg() string }); ok {
    // write the message to the http response
    io.WriteString(response, msg.ResponseMsg())
}

TL;DR you would need to handle all the cases, but you can create error types that make the code much easier to work with!

like image 109
Zak Avatar answered Jan 30 '23 12:01

Zak