Introduction
In part I of this post, we learned about the error interface and how the standard library provides support for creating error interface values via the errors package. We also learned how to work with error interface values and use them to identify when an error has occured. Finally, we saw how some packages in the standard library export error interface variables to help us identify specific errors.



Knowing when to create and use custom error types in Go can sometimes be confusing. In most cases, the traditional error interface value provided by the errors package is enough for reporting and handling errors. However, sometimes the caller needs extra context in order to make a more informed error handling decision. For me, that is when custom error types make sense.

In this post, we are going to learn about custom error types and look at two use cases from the standard library where they are used. Each use case provides an interesting perspective for when and how to implement a custom error type. Then we will learn how to identify the concrete type of the value or pointer stored within an error interface value, and see how that can help us make more informed error handling decisions.

The net Package
The net package has declared a custom error type called OpError. Pointers of this struct are used by many of the functions and methods inside the package as the concrete type stored within the returned error interface value:

  Listing 1.1  
http://golang.org/src/pkg/net/dial.go

01 func Listen(net, laddr string) (Listener, error) {
02     la, err := resolveAddr("listen", net, laddr, noDeadline)
03     if err != nil {
04         return nil, &OpError{Op: "listen", Net: net, Addr: nil, Err: err}
05     }
06     var l Listener
07     switch la := la.toAddr().(type) {
08     case *TCPAddr:
09         l, err = ListenTCP(net, la)
10     case *UnixAddr:
11         l, err = ListenUnix(net, la)
12     default:
13         return nil, &OpError{Op: "listen", Net: net, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: laddr}}
14     }
15     if err != nil {
16         return nil, err // l is non-nil interface containing nil pointer
17     }
18     return l, nil
19 }

Listing 1.1 shows the implementation of the Listen function from the net package. We can see that on lines 04 and 13, pointers of the OpError struct are created and passed in the return statement for the error interface value. Since pointers of the OpError struct implement the error interface, the pointer can be stored in an error interface value and returned. What you don’t see is that on lines 09 and 11, the ListenTCP and ListenUnix functions can also return pointers of the OpError struct, which are stored in the returned error interface value.

Next, let’s look at the declaration of the OpError struct:

  Listing 1.2  
http://golang.org/pkg/net/#OpError

01 // OpError is the error type usually returned by functions in the net
02 // package. It describes the operation, network type, and address of
03 // an error.
04 type OpError struct {
05     // Op is the operation which caused the error, such as
06     // "read" or "write".
07     Op string
08
09     // Net is the network type on which this error occurred,
10     // such as "tcp" or "udp6".
11     Net string
12
13     // Addr is the network address on which this error occurred.
14     Addr Addr
15
16     // Err is the error that occurred during the operation.
17     Err error
18 }

Listing 1.2 shows the declaration of the OpError struct. The first three fields on lines 07, 11 and 14 provide context about the network operation being performed when an error occurs. The fourth field on line 17 is declared as an error interface type. This field will contain the actual error that occurred and the value of the concrete type in many cases will be a pointer of type errorString.

Another thing to note is the naming convention for custom error types. It is idiomatic in Go to postfix the name of a custom error type with the word Error. We will see this naming convention used again in other packages.

Next, let’s look at the implementation of the error interface for the OpError struct:

  Listing 1.3  
http://golang.org/src/pkg/net/net.go

01 func (e *OpError) Error() string {
02     if e == nil {
03         return "<nil>"
04     }
05     s := e.Op
06     if e.Net != "" {
07         s += " " + e.Net
08     }
09     if e.Addr != nil {
10         s += " " + e.Addr.String()
11     }
12     s += ": " + e.Err.Error()
13     return s
14 }

The implementation of the error interface in listing 1.3 shows how the context associated with the error is used to generate a contextual error message. Binding context with the error provides extended error information and can help the caller make more informed decisions about how to handle the error.

The json Package
The json package performs the decoding of data from JSON to native Go types and vice versa. All possible errors that can be returned from the package are generated internally. Maintaining the context associated with an error is critical for this package or it wouldn’t be able to properly report what has happened. There are several custom error types in the json package and these types can be returned by the same functions and methods.

Let’s look at one of these custom error types:

  Listing 1.4  
http://golang.org/src/pkg/encoding/json/decode.go

01 // An UnmarshalTypeError describes a JSON value that was
02 // not appropriate for a value of a specific Go type.
03 type UnmarshalTypeError struct {
04     Value string // description of JSON value
05     Type reflect.Type // type of Go value it could not be assigned to
06 }
07
08 func (e *UnmarshalTypeError) Error() string {
09     return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
10 }

Listing 1.4 shows the declaration of the UnmarshalTypeError struct and the implementation of the error interface. This struct is used to report errors that occur when a value can’t be decoded into a specific Go type. The struct contains two fields, one called Value on line 04 that contains the value attempted to be decoded and one called Type on line 05 which contains the Go type that the value could not be converted to. The implementation of the error interface on line 08 takes the context of the error and produces a proper error message.

In this case, the type itself provides the context for the error. The name of this type is called UnmarshalTypeError and that is the context in which it is used. When there are errors associated with unmarshaling types, pointers of this struct are stored within the returned error interface value.

When there are invalid arguments passed into an unmarshal call, a pointer of concrete type InvalidUnmarshalError is stored within the error interface value that is returned:

  Listing 1.5  
http://golang.org/src/pkg/encoding/json/decode.go

01 // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal.
02 // (The argument to Unmarshal must be a non-nil pointer.)
03 type InvalidUnmarshalError struct {
04     Type reflect.Type
05 }
06
07 func (e *InvalidUnmarshalError) Error() string {
08     if e.Type == nil {
09         return "json: Unmarshal(nil)"
10     }
11
12     if e.Type.Kind() != reflect.Ptr {
13         return "json: Unmarshal(non-pointer " + e.Type.String() + ")"
14     }
15     return "json: Unmarshal(nil " + e.Type.String() + ")"
16 }

Listing 1.5 shows the declaration of the InvalidUnmarshalError struct and the implementation of the error interface. Once again, the type itself provides the context for the error. The state being maintained helps to produce a proper error message and provides context to help the caller make more informed error handling decision.

Concrete Type Identification
In the case of the Unmarshal function from the json package, there is the potential for a pointer of type UnmarshalTypeError, InvalidUnmarshalError or errorString to be stored within the error interface value that is returned:

  Listing 1.6  
http://golang.org/src/pkg/encoding/json/decode.go

01 func Unmarshal(data []byte, v interface{}) error {
02     // Check for well-formedness.
03     // Avoids filling out half a data structure
04     // before discovering a JSON syntax error.
05     var d decodeState
06     err := checkValid(data, &d.scan)
07     if err != nil {
08         return err
09     }
10
11     d.init(data)
12     return d.unmarshal(v)
13 }
14
15 func (d *decodeState) unmarshal(v interface{}) (err error) {
16     defer func() {
17         if r := recover(); r != nil {
18             if _, ok := r.(runtime.Error); ok {
19                 panic(r)
20             }
21             err = r.(error)
22         }
23     }()
24
25     rv := reflect.ValueOf(v)
26     if rv.Kind() != reflect.Ptr || rv.IsNil() {
27         return &InvalidUnmarshalError{reflect.TypeOf(v)}
28     }
29
30     d.scan.reset()
31     // We decode rv not rv.Elem because the Unmarshaler interface
32     // test must be applied at the top level of the value.
33     d.value(rv)
34     return d.savedError
35 }

Listing 1.6 shows how the returned error interface value for the Unmarshal call can potentially store pointers of different concrete types. On line 27, the unmarshal method returns a pointer of type InvalidUnmarshalError and then on line 34, the value of the savedError field from the decodeState variable is returned. This value can be pointers of several different concrete types.

Knowing that the json package is using the custom error type as the context for the error, how can we identify the type of the concrete value to make a more informed decision about handling the error?

Let’s start with a program that causes the Unmarshal function to return an error interface value with a concrete pointer type of UnmarshalTypeError:

  Listing 1.7  
http://play.golang.org/p/FVFo8mJLBV

01 package main
02
03 import (
04     "encoding/json"
05     "fmt"
06     "log"
07 )
08
09 type user struct {
10     Name int
11 }
12
13 func main() {
14     var u user
15     err := json.Unmarshal([]byte(`{"name":"bill"}`), &u)
16     if err != nil {
17         log.Println(err)
18         return
19     }
20
21     fmt.Println("Name:", u.Name)
22 }

Output:
2009/11/10 23:00:00 json: cannot unmarshal string into Go value of type int

Listing 1.7 shows a sample program that attempts to unmarshal a JSON document into a Go type. The JSON document on line 15 contains a field named name with a string value of bill. Since the Name field in the user type declared on line 09 is declared as an integer, the Unmarshal function returns an error interface value stored with a concrete pointer type of UnmarshalTypeError.

Now let’s make some changes to the program in listing 1.7 so the same Unmarshal call returns an error interface value that is storing a different concrete pointer type:

  Listing 1.8  
http://play.golang.org/p/n8dQFeHYVp

01 package main
02
03 import (
04     "encoding/json"
05     "fmt"
06     "log"
07 )
08
09 type user struct {
10     Name int
11 }
12
13 func main() {
14     var u user
15     err := json.Unmarshal([]byte(`{"name":"bill"}`), u)
16     if err != nil {
17         switch e := err.(type) {
18         case *json.UnmarshalTypeError:
19             log.Printf("UnmarshalTypeError: Value[%s] Type[%v]\n", e.Value, e.Type)
20         case *json.InvalidUnmarshalError:
21             log.Printf("InvalidUnmarshalError: Type[%v]\n", e.Type)
22         default:
23             log.Println(err)
24         }
25         return
26     }
27
28     fmt.Println("Name:", u.Name)
29 }

Output:
2009/11/10 23:00:00 json: Unmarshal(non-pointer main.user)
2009/11/10 23:00:00 InvalidUnmarshalError: Type[main.user]

The sample program in listing 1.8 has a few changes from listing 1.7. On line 15, we now pass the value of the variable u instead of its address to the Unmarshal function. This change causes the Unmarshal function to return an error interface value that is storing a concrete pointer type of InvalidUnmarshalError.

Then we do something interesting on lines 17 through 24:

  Listing 1.9  
17         switch e := err.(type) {
18         case *json.UnmarshalTypeError:
19             log.Printf("UnmarshalTypeError: Value[%s] Type[%v]\n", e.Value, e.Type)
20         case *json.InvalidUnmarshalError:
21             log.Printf("InvalidUnmarshalError: Type[%v]\n", e.Type)
22         default:
23             log.Println(err)
24         }

switch statement on line 17 is added to identify the concrete type of the pointer that was stored inside the returned error interface value. Notice how the keyword type is used within the interface value conversion syntax. We are also able to extract the value of the concrete type at the same time and use it in each case statement.

The case statements on lines 18 and 20 check for the specific concrete types and perform logic associated with handling those errors. This is the idiomatic way in Go to identify the concrete type of the value or pointer stored within an error interface value from a set of concrete types.

Conclusion
The error interface values we return should contain information about a specific negative condition that has occurred within the scope of a function or method that impacts the caller. It must provide enough information so the caller can act in an informed way. Usually a simple message is enough, but sometimes the caller needs more.

We saw one case from the net package where a custom error type was declared to wrap the original error and the context associated with that error. In the json package, we saw the use of custom error types that provide both the context of the error and associated state. In both cases, the need to maintain context associated with an error was a deciding factor.

When the traditional error interface value created by the errors package provides enough context for the error, use it. It is used throughout the standard library and is usually all you need. When extra context is required to help the caller make a more informed decision, take a cue from the standard library and build your own custom error types.

Trusted by top technology companies

We've built our reputation as educators and bring that mentality to every project. When you partner with us, your team will learn best practices and grow along the way.

30,000+

Engineers Trained

1,000+

Companies Worldwide

12+

Years in Business