magum
magum

Reputation: 626

Go - How can I subtype a wrapped error class?

I am wrapping errors (to add context) and am afterwards distinguishing between two errors. This is a scenario that I am currently using for tests. (Did the function recognise the error correctly?) My question is how I can reduce the verbosity.

I have two functions that create different errors:

func a() error {
    return errors.New("a")
}

func b() error {
    return errors.New("b")
}

They are both called by a third function that propagates the erorr.

func doStuff() error {
    err := a()
    if err != nil {
        return WrapA{err}
    }
    err = b()
    if err != nil {
        return WrapB{err}
    }
    return nil
}

In my main function, I distinguish between both errors.

func main() {
    fmt.Println("Hello, playground")
    err := doStuff()
    
    switch err.(type) {
        case WrapA:
            fmt.Println("error from doing a")
        case WrapB: 
            fmt.Println("error from doing b")
        case nil:
            fmt.Println("nil")
        default:
            fmt.Println("unknown")
    }
}

So far, so good. Unfortunately, to implement WrapA and WrapB, I need a lot of code:

type WrapA struct {
    wrappedError error
}

func (e WrapA) Error() string {
    return e.wrappedError.Error()
}

func (e WrapA) Unwrap() error {
    return e.wrappedError
}

type WrapB struct {
    wrappedError error
}

func (e WrapB) Error() string {
    return e.wrappedError.Error()
}

func (e WrapB) Unwrap() error {
    return e.wrappedError
}

In other languages, I would create a single Wrap struct and let WrapA and WrapB inherit from Wrap. But I don't see a way to do this in Go.

Any ideas on how to reduce the clutter?

Go Playground https://play.golang.org/p/ApzHC_miNyV

EDIT: After seeing jub0bs answer, I want to clarify: Both a() and b() are callbacks I have no control over. They may return various errors. This is the reason why I wrap them.

Upvotes: 2

Views: 1043

Answers (3)

jub0bs
jub0bs

Reputation: 66244

If I understand the problem correctly, you can indeed simplify things:

  • Define a and b as package-level error variables for ease and better performance.
  • Unless you require programmatic access to values only accessible in the context of the error that you're wrapping, you most likely don't need to declare those custom WrapA and WrapB error types. Instead, you can simply use the %w verb in conjunction with fmt.Errorf to produce a new error value that wraps the lower-level error.
  • You can then use errors.Is within a tagless switch to inspect the cause of the higher-level error returned by your doStuff function.

(Playground)

package main

import (
    "errors"
    "fmt"
)

var (
    a = errors.New("a")
    b = errors.New("b")
)

func doStuff() error {
    err := a
    if err != nil {
        return fmt.Errorf("%w", err)
    }
    err = b
    if err != nil {
        return fmt.Errorf("%w", err)
    }
    return nil
}

func main() {
    fmt.Println("Hello, playground")
    switch err := doStuff(); {
    case errors.Is(err, a):
        fmt.Println("error from doing a")
    case errors.Is(err, b):
        fmt.Println("error from doing b")
    case err == nil:
        fmt.Println("nil")
    default:
        fmt.Println("unknown")
    }
}

Upvotes: 5

user4466350
user4466350

Reputation:

Adding a structured error version which composes a type Wrap along various more specific error types;

package main

import (
    "errors"
    "fmt"
)

func a() error {
    return errors.New("something more specific broke in a")
}

func b() error {
    return errors.New("something more specific broke in b")
}

func doStuff() error {
    err := a()
    if err != nil {
        return ErrA{
            Wrap:        Wrap{err: err},
            SpecficProp: "whatever",
        }
    }
    err = b()
    if err != nil {
        return ErrB{
            Wrap:         Wrap{err: err},
            SpecficProp2: "whatever else",
        }
    }
    return nil
}

func main() {
    fmt.Println("Hello, playground")
    err := doStuff()

    if target := (ErrA{}); errors.As(err, &target) {
        fmt.Printf("%v\n", target)
    } else if target := (ErrB{}); errors.As(err, &target) {
        fmt.Printf("%v\n", target)
    } else if err != nil {
        fmt.Println("unknown")
    } else {
        fmt.Println("nil")
    }
}

type Wrap struct {
    err error
}

func (e Wrap) Error() string {
    return e.err.Error()
}

func (e Wrap) Unwrap() error {
    return e.err
}

type ErrA struct {
    Wrap
    SpecficProp interface{}
}

func (e ErrA) Error() string {
    return fmt.Sprintf("got error of kind A with %#v, plus %T", e.SpecficProp, e.Unwrap())
}

type ErrB struct {
    Wrap
    SpecficProp2 interface{}
}

func (e ErrB) Error() string {
    return fmt.Sprintf("got error of kind B with %#v, plus %T", e.SpecficProp2, e.Unwrap())
}

Upvotes: 2

Simon
Simon

Reputation: 32873

Use constant errors if you can. Then you could switch on the error itself.

Upvotes: 0

Related Questions