[Golang Error handling] The best way to separate processing according to the type of error

Introduction

I tried to find out what kind of method is available when I want to change the processing depending on the type of error in Golang.

That's because ** Golang doesn't support try-catch. ** ** So I started writing this article when I tried to find out how Golang could implement Java try-catch-like features.

About the overall outline of the article. -First of all, I will explain four frequently used functions of the error package. -Next, we will try three possible methods to change the processing depending on the type of error. -And the conclusion of the best way to divide the processing according to the type of error. We will proceed in this order.

4 frequently used functions of errors package

The standard error package does not have the following functions.

--Determining the type of error that occurred first --Get stack trace information

So basically I use the errors package. Now let's take a look at the features of errors.

Function 1 func New (message string) error

It is used when ** simply generating an error ** by specifying it in the error message string as shown below.

err := errors.New("Ella~That's right~Hmm")
fmt.Println("output: ",err)
// output:Ella~That's right~Hmm

Function 2 func Errorf (format string, args ... interface {}) error

It is used to generate an error ** by specifying the ** format and error message string as shown below.

err := errors.Errorf("output: %s", "Ella~That's right~Hmm")
fmt.Printf("%+v", err)
// output:Ella~That's right~Hmm

Function 3 func Wrap (err error, message string) error

It is used when wrapping the original error as shown below.

err := errors.New("repository err")
err = errors.Wrap(err, "service err")
err = errors.Wrap(err, "usecase err")
fmt.Println(err)
// usecase err: service err: repository err

** Important feature **, so I'll explain it in a little more detail. For example, even if there is a deep hierarchy such as usecase layer → service layer → repository layer By wrapping the first error, you can bring the lower error information to the upper level. As a result, it is easier to identify the cause of the ** error. ** **

Also, I don't care this time, but it seems that it is common to include the function name in the error message in order to quickly identify the cause. Since the messages are connected to each other, it is also necessary to assemble them so that they become natural error messages.

Function 4 func Cause (err error) error

Used to pull the first error message from the wrapped error. ** Very useful in identifying the cause of the first error. ** **

err := errors.New("repository err")
err = errors.Wrap(err, "service err")
err = errors.Wrap(err, "usecase err")
fmt.Println(errors.Cause(err))
// repository err

Seeking the best error handling

Here are three error handling methods. Let's look at what is the problem one by one and how we can solve it.

--Method 1 Judgment based on error value --Method 2 Judgment by error type --Method 3 Judgment using the interface

In conclusion, I think the best method is ** Judgment using Method 3 interface **. (I would appreciate it if you could point out in the comments if there is something like this!)

Method 1 Judgment based on error value

code

var (
	//Define possible errors
	ErrHoge = errors.New("this is error hoge")
	ErrFuga = errors.New("this is error fuga")
)

func Function(str string) error {
	//Returns different errors depending on the process
	if str == "hoge" {
		return ErrHoge
	}else if str == "fuga" {
		return ErrFuga
	}
	return nil
}


func main()  {
	err := Function("hoge")
	
	switch err {
	case ErrHoge:
		fmt.Println("hoge")
	case ErrFuga:
		fmt.Println("fuga")
	}
}

Summary

Whether the ** return value ** of the function (Function) is ErrHoge or ErrFuga is determined by the switch statement, and the processing is distributed. ** I think this is a bad way **. The problems are the following three points.

--It is necessary to fix the error message returned by Function --Strong dependencies between packages --~~ err needs to be published to the outside ~~

If the above points cause problems, you should consider other methods.

Method 2 Judgment by error type

code

type Err struct {
	err error
}

func (e *Err) Error() string {
	return fmt.Sprint(e.err)
}

type ErrHoge struct {
	*Err
}
type ErrFuga struct {
	*Err
}

func Function(str string) error {
	//Returns different errors depending on the process
	if str == "hoge" {
		return ErrHoge{&Err{errors.New("this is error hoge")}}
	} else if str == "fuga" {
		return ErrFuga{&Err{errors.New("this is error fuga")}}
	}
	return nil
}

func main() {
	err := Function("hoge")

	switch err.(type) {
	case ErrHoge:
		fmt.Println("hoge")
	case ErrFuga:
		fmt.Println("fuga")
	}
}

Summary

The switch statement determines whether the ** return type ** of the Function is ErrHoge or ErrFuga, and distributes the processing. This method is also not very good, but since the value judgment is changed to the type judgment, The following issues of ** Judgment by error value ** have been resolved.

--It is necessary to fix the error message returned by Function

There are two remaining issues.

--Strong dependencies between packages -~~ It is necessary to expose the structure to the outside ~~

Method 3 Judgment using the interface

code

type temporary interface {
	Temporary() bool
}

func IsTemporary(err error) bool {
	te, ok := errors.Cause(err).(temporary)
	return ok && te.Temporary()
}

type Err struct {
	s string
}

func (e *Err) Error() string { return e.s }

func (e *Err) Temporary() bool { return true }

func Function(str string) error {
	//Returns different errors depending on the process
	if str == "hoge" {
		return &Err{"this is error"}
	} else {
		errors.New("unexpected error")
	}
	return nil
}

func main() {
	err := Function("hoge")

	if IsTemporary(err) {
		fmt.Println("Expected error:", err)
	} else {
		fmt.Println(err)
	}
}

The code is a little complicated, so I will explain it. Err implements the temporary interface. Therefore, it is possible to narrow down "the one that implements the temporary interface and the return value is true" by the judgment of ```IsTemporary ()` `` of the processing on the user side.

Also, by using errors.cause as shown below, even if the error is wrapped, it is possible to distinguish ** whether the first error occurred implements the temporary interface **.

//See if the returned err implements temporary
te, ok := err.(temporary)
//See if the first error that occurred implements temporary (← recommended)
te, ok := errors.Cause(err).(temporary)

Therefore, by looking at the result of ```Is Temporary (err) `` `, you can sort the processing according to the error (root cause) that occurred first.

Summary

This method was able to solve the remaining two problems with ** error type judgment **.

--Strong dependency between packages ⇒ Depends on temporary interface -~~ It is necessary to expose the structure to the outside ~~

With this, if try-catch in Java is golang, it seems that it can be implemented as follows.

java


public static void main(String[] args) {
	try {
        // ...
	} catch (ArithmeticException e) {
		// ...
	} catch (RuntimeException e) {
		// ...
	} catch (Exception e) {
		// ...
	}
}

golang


func main() {
	err := Function("//....")

	if IsArithmeticException(err) {
		// ...
	}
	if IsRuntimeException(err) {
		// ...
	}
	if IsException(err) {
		// ...
	}
	// ...
}

Finally

I've talked about three error handling in Golang. By making a judgment using the interface of method 3, it seems that there are few problems and the processing can be divided according to the type of error.

I'm still studying, so I'd appreciate it if you could let me know in the comments if there are other ways to handle errors.

This article was very helpful. https://dave.cheney.net/tag/error-handling

Postscript

・ 2018/11/10 "The structure needs to be exposed to the outside" This description has been deleted. By not exposing the structure to the outside, it is possible to prevent judgment by error type, This is because making it a private structure causes a problem that the value of the field cannot be referenced from the outside.

Recommended Posts

[Golang Error handling] The best way to separate processing according to the type of error
An easy way to measure the processing speed of a disk recognized by Linux
Introduction to machine learning ~ Let's show the table of K-nearest neighbor method ~ (+ error handling)
Easy way to check the source of Python modules
Try to get the contents of Word with Golang
Automatically select BGM according to the content of the conversation
Switch the setting value of setting.py according to the development environment
How to increase the processing speed of vertex position acquisition
Error handling after stopping the download of learned data of VGG16
Change the volume of Pepper according to the surrounding environment (sound)
Dot according to the image
Consider the speed of processing to shift the image buffer with numpy.ndarray