0

I was going through this article by Andrew Gerrand and the author mentions that since error is an interface

you can use arbitrary data structures as error values, to allow callers to inspect the details of the error.

and gives this example

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

but didn't really show how it is implemented, only discussing its possible use in type assertions. Although I imagined you could simply just return it with a value when the situation occurs as opposed to using fmt.Errorf as shown in the example below

package main

//trying to define a custom error type in Go
import "fmt"

type NegativeError float64

func (f NegativeError) Error() string {
    return fmt.Sprintf("%v: temperature can't go below absolute zero", f)
}
func compute(a, b float64) (float64, error) {
    var t float64
    t = a - b
    if t < 0 {
        return t, NegativeError(t)

    }
    return t, nil
}
func main() {
    fmt.Println(compute(4, 5))
}

But this doesn't work and gives rise to the error below

runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc0200e13a0 stack=[0xc0200e0000, 0xc0400e0000]
fatal error: stack overflow

This seems to only be an issue when you implement the error interface's Error() method, but it works when you change the method name. (shown below)

package main

//trying to define a custom error type in Go
import "fmt"

type NegativeError float64

func (f NegativeError) Err() string {
    return fmt.Sprintf("%v: temperature can't go below absolute zero", f)
}
func compute(a, b float64) (float64, string) {
    var t float64
    t = a - b
    if t < 0 {
        return t, NegativeError(t).Err()

    }
    return t, ""
}
func main() {
    fmt.Println(compute(4, 5))
}

or when you implement the error interface on a struct type and not a float. (shown below)

package main

//trying to define a custom error type in Go
import (
    "fmt"
)

type NegativeError struct {
    value float64
}

func (f NegativeError) Error() string {
    return fmt.Sprintf("%v: temperature can't go below absolute zero", f.value)
}
func compute(a, b float64) (float64, error) {
    var t float64
    t = a - b
    if t < 0 {
        return t, NegativeError{t}

    }
    return t, nil
}
func main() {
    fmt.Println(compute(4, 5))
}

The obvious easy fix is to implement the interface on a struct type, but I would like to know if anyone else has experienced this particular error and how they handled it, or if this is the wrong way to go about it. Thanks.

3
  • 7
    You’re using %v to format the error, which calls the Error() method again. Commented Mar 24, 2024 at 17:27
  • 2
    go vet (run automatically in the playground) will warn you about doing this. Commented Mar 24, 2024 at 19:45
  • link to the explanation: pkg.go.dev/fmt 4. If an operand implements the error interface, the Error method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any). 5. If an operand implements method String() string, that method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any). Commented Mar 30, 2024 at 13:53

1 Answer 1

2

As already mentioned in the comments, the Error() method calls itself as it is used to format an error type to a string if you pass the error type itself to fmt.Sprintf.

There are several ways to fix this:

  1. Convert f to the underlying float64 type and pass that into fmt.Sprintf:
func (f NegativeError) Error() string {
    return fmt.Sprintf("%v: temperature can't go below absolute zero", float64(f))
}
  1. Don't use %v but instead %f. The difference is that %v will try to stringify the value and for error types that means calling the Error() string method. %f will treat the f as a float64, which it is so it will work:
func (f NegativeError) Error() string {
    return fmt.Sprintf("%f: temperature can't go below absolute zero", f)
}

From the solutions you've suggested, the solution using a struct is the most optimal approach IMO (apart from the 2 I mentioned above). It opens up the possibility to add more data to the error later, too. Especially error wrapping comes to mind which for me all my custom errors need to be able to do.

Error wrapping: The error can contain another error and has a method to Unwrap the original error (as defined by the standard library errors package). It is needed to be able to get to the original error for wrapped errors. e.g. fmt.Errorf("some error: %w", err) creates a new error with err wrapped inside.

Sign up to request clarification or add additional context in comments.

1 Comment

Here one of my custom errors in case you want to have a look: github.com/tehsphinx/errs/blob/main/fields.go. It contains a map of field values that I extract from the error for structured logging. (And it adds a stack if there is none in the error yet.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.