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 PackageThe
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 PackageThe
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 IdentificationIn 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 }
A
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.
ConclusionThe
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.