Search code examples
goerror-handlingruntime-errorstack-overflow

stack overflow error occurs only when converting to a type with the Error method defined


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.


Solution

  • 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.