Home > Back-end >  How do I work out what type of error my error is in go?
How do I work out what type of error my error is in go?

Time:10-16

I'm not sure exactly how to phrase this question, and I've seen others ask similar but not really come up with answers (which tells me I'm asking the wrong question, but I'm not sure how else to approach this).

I'm trying to learn some basic Go, and I've come unstuck at the first hurdle.

In my test code, I'm doing a basic http GET to a domain that doesn't exist to trigger a DNS warning. I worked out that err.error() returns a string, so to assert whether it was a DNS error, I used string comparison:

resp, err := http.Get(link)
if err != nil {
    if strings.Contains(err.Error(), "no such host") == true {
        return "no such host"
    }
}

This is obviously hacky, so I did some googling to see if there is a better way to work out what kind of error was raised, and I found the following SO answer:

How can I check specific golang net/http error code?

Package "errors" has functions As, Is to unwrap specific error types, and package "net" has a *DNSError type. So:

var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
    ...
}

This code works, but I have absolutely zero idea how this conclusion was reached. Below is how I'm approaching trying to understand this, and I'd like to know where I'm going wrong. I understand (vaguely) what .Is() and .As() are doing, but what I don't understand is how to work out what error "type" to provide those functions without guessing or prior knowledge.

I looked at the client.get() documentation here which says:

Any returned error will be of type *url.Error.

some more googling and I found that I need to cast(?) the error to that type to work with it:

urlErr := err.(*url.Error)

the *url.Error contains:

&url.Error{Op:"Get", URL:"http://doesnotexistkjdfhgsdfsdf.com", Err:(*net.OpError)(0xc00021a2d0)}

so I then look at the net.OpError contained in the url.Error:

netOpError := urlErr.Err.(*net.OpError)
fmt.Printf("Net Op Error contains: %#v\n", netOpError)
---
Net Op Error contains: &net.OpError{Op:"dial", Net:"tcp", Source:net.Addr(nil), Addr:net.Addr(nil), Err:(*net.DNSError)(0xc0001a0040)}

I then do the same thing and "unpack" the net.DNSError contained within net.OpError:

dnsError := netOpError.Err.(*net.DNSError)
fmt.Printf("DNSError contains: %#v\n", dnsError)
---
DNSError contains: &net.DNSError{Err:"dial udp 169.254.169.254:53: connect: no route to host", Name:"doesnotexistkjdfhgsdfsdf.com", Server:"169.254.169.254:53", IsTimeout:false, IsTemporary:true, IsNotFound:false}

The net.DNSError doesn't "contain" any other errors so to me, this suggests it's the bottom of the chain and the "real" error (or, at least, one I wanted to work with).

Thing is, this is not a viable approach, and I don't understand how we're supposed to approach this. Before the initial SO article I found, I had no idea that net.DNSError is a thing, or that my error could be of that "type".

If you didn't know a particular error type exists, and that a function call could possibly be of that type, how would you know?

I have a very limited understanding of interfaces and types in general in Go, which I'm sure isn't helping here, but to me there seems to be a huge leap between having an error and knowing what kind of error to check it could be. I hope this question makes sense!

CodePudding user response:

First, I'd suggest reading this blog post: https://go.dev/blog/go1.13-errors

Short answer to your question: you're correct, to check that a function returns net.DNSError and access its internals you can use errors.As function:

rsp, err := http.Get(link)
dnsErr := new(net.DNSError)
if errors.As(err, &dnsErr) {
  // use dnsErr here
}

Update: conceptually, it's supposed that you know the type of error you can handle: so you either handle some particular error if you can handle it or wrap and return it to upper level. It's a common practice when working with errors/exceptions in other languages too. So my advice: handle only such exceptions that you know how to process. In case of HTTP requests, it's usually http errors with status codes, DNS errors are usually returned to the caller func.


This is some details with examples for functions from errors package.

You can use errors.As and errrors.Is from errors package. For instance, if you have a custom type of error:

type myError struct {
    code int
}

You can check unknown error by reference with errors.Is():

var fooErr = &myError{1}

func foo() (int, err) {...}

func main() {
    _, err := foo()
    fmt.Printf("is fooErr? %v\n", errors.Is(err, fooErr))
}

or if you want to implement custom logic for comparing your errors (e.g. by code in this example), you can add Is method to your error type:

func (e *myError) Is(err error) bool {
    as := new(myError)
    // I'll show As method later
    if !errors.As(err, &as) {
        return false
    }
    return e.code == as.code
}

func (e *myError) Is(err error) bool {
    as := new(myError)
    if !errors.As(err, &as) {
        return false
    }
    return e.code == as.code
}

Also, Is method can "unwrap" your error from wrapper type, e.g. if you want to create a composition of errors, you may add a new struct with Unwrap method for that or use fmt.Errorf method with %w parameter:

type errWrap struct {
    origin error
}

func (e *errWrap) Unwrap() error {
    return e.origin
}

func (e *errWrap) Error() string {
    return fmt.Sprintf("wraps error '%s'", e.origin.Error())
}

var fooErr = &myError{1}

func foo() (int, error) {
    return 0, &errWrap{origin: fooErr}
}

func main() {
    _, err := foo()
    fmt.Printf("err == myError? %v\n", err == fooErr) // false
    fmt.Printf("err is fooErr? %v\n", errors.Is(err, &myError{1})) // true
}

Or using fmt.Errorf:

    err := fmt.Errorf("wraps: %w", fooErr)
    fmt.Printf("err == myError? %v\n", err == fooErr) // false
    fmt.Printf("err is fooErr? %v\n", errors.Is(err, &myError{1})) // true

Another important method is errors.As, it works similar: you either have the exact type of your error to check or you implement As method on your error or error is wrapped by another error:

    err := &myError{1}
    as := new(myError)
    errors.As(err, &as)
    fmt.Printf("code: %v\n", as.code) // code: 1

Or with As method:


type anotherError struct {
    anotherCode int
}

func (e *anotherError) Error() string {
    return fmt.Sprintf("code: %d", e.anotherCode)
}

func (e *myError) As(target interface{}) bool {
    if out, ok := target.(**anotherError); ok {
        (*out).anotherCode = e.code
        return true
    }
    return false
}

func main() {
    err := &myError{1}
    as := new(anotherError)
    errors.As(err, &as)
    fmt.Printf("code: %v\n", as.anotherCode) // code: 1
}

And same for wrapping:

err := fmt.Errorf("wraps: %w", &myError{1})
as := new(myError)
errors.As(err, &as)
fmt.Printf("code: %v\n", as.code) // code: 1

CodePudding user response:

What does a user do when loading a web-page and the page fails to load? They hit reload. If it keeps failing, the user eventually studies the error message, as it could be one of many reasons:

  • DNS error (client)
  • 504 gateway error (server)
  • 400 (user: bad request)
  • 404 (user: not found)
  • 401 (user unauthorized)

How the user reacts to these errors determines whether to: give-up; try again; or try again in a different manner. Your question began with the http.Get errors (not http status codes) - but the same principle applies.

So, what should you do if the request fails? If the http.Get is a critical part of a larger task, then the whole task must fail. If the larger task is expensive to run, you may want to retry the http.Get (exponential backup retry etc.) before failing. Network errors are typically logged as there's very little policy decisions one can make to determine if a retry will work or not (client DNS vs. server 504).

In a REST-API server implementation, for example, the authentication middleware will return errors for login issues. Here is where the type of error is important and how one can react to it:

  • authentication-DB is down
    • server: log full error; email admin etc.
    • client: receives terse 501 (service unavailable)
  • invalid credentials
    • server: log full error
    • client: receives terse 401 (unauthorized)

the client is only given a very brief summary that the operation failed, but the server logs the full details of the error and will react to it (alert admin, email user of locked out account or even a successful login, but from an unrecognized device).

CodePudding user response:

Errors in Go are essentially just strings put into an Error object, the library you are using should have variables as the error which you can then use as a comparator, for example in the library discordgo, the file discord.go has a variable ErrMFA which is the Error object they should return in the case that they need to. Then you can use:

if err == discordgo.ErrMFA {
    log.Println("Caught MFA error")
}

If they do not have these already, you could add them yourself, otherwise you must do string comparisons.

  • Related