[Golang] Error handling

2021-12-03 hit count image

Let's see how to create errors and handle them in Golang.

Outline

In this blog post, I will show you how to handle errors in Golang. You can see the full source code of this blog post on the link below.

Error handling

In programming, errors can occur anywhere, anytime.

  • Bug: Human error(programmer’s mistake), program malfunction
  • External environment: Machine problem(out of memory), physical problem(power off)

There are two ways to handle errors. One is to make the program exits, and the other is to handle the error and continue the program.

Return error

Golang returns errors as follows so that users of the code can handle the errors.

import "os"

file, err := os.Open(filename)
file, err := os.Create(filename)

To check this, create the main.go file and modify it like the below.

package main

import (
  "bufio"
  "fmt"
  "os"
)

func ReadFile(filename string) (string, error) {
  file, err := os.Open(filename)

  if err != nil {
    return "", err
  }

  defer file.Close()

  rd := bufio.NewReader(file)
  line, _ := rd.ReadString('\n')

  return line, nil
}

func WriteFile(filename string, data string) error {
  file, err := os.Create(filename)
  if err != nil {
    return err
  }

  defer file.Close()

  _, err = fmt.Fprintf(file, data)
  return err
}

const filename string = "data.txt"

func main() {
  line, err := ReadFile(filename)
  if err != nil {
    err = WriteFile(filename, "Hello, World!\n")
    if err != nil {
      fmt.Println("Failed to create file!", err)
      return
    }
    line, err = ReadFile(filename)
    if err != nil {
      fmt.Println("Failed to read file!", err)
      return
    }
  }
  fmt.Println("File contents: ", line)
}

When the code is executed, you can see the following result.

# go run main.go
File contents:  Hello, World!

So, when you create a function and provide it, it’s better to return errors for making the user can handle the errors.

Return custom error

You can make the custom error like the following in Golang.

fmt.Errorf(formatter string, ...interface {})

Also, you can create errors with the errors package like the below.

import "errors"

errors.New(text string) error

To check this, modify the main.go file like the below.

package main

import (
  "errors"
  "fmt"
)

func errorTest(i int) (int, error) {
  switch i {
  case 0:
    return i, fmt.Errorf("Error: %d", i)
  case 1:
    return i, errors.New("Error")
  default:
    return i, nil
  }
}

func main() {
  i, err := errorTest(0)
  if err != nil {
    fmt.Println(err)
  }

  i, err = errorTest(1)
  if err != nil {
    fmt.Println(err)
  }

  i, err = errorTest(2)
  if err != nil {
    fmt.Println(err)
  }
  fmt.Println(i)
}

When you execute the code, you can see the result like the below.

# go run main.go
Error: 0
Error
2

Normally, if you want to show the specific variables in the error message, you can use the fmt.Sprintf function. And if you want to show the simple string message, you can use the errors.New function.

Error type

If you want to add more information to the error, you can use the error type.

type error interface {
  Error() string
}

In Golang, you can use the error type that implements the Error method. For example, if you define the PasswordError type like the below and implement the Error method, you can use it for the error type.

type PasswordError struct {
  RequiredLen int
  Len         int
}

func (e PasswordError) Error() string {
  return "Password is too short."
}

To check this, modify the main.go file like the below.

package main

import (
  "fmt"
)

type PasswordError struct {
  RequiredLen int
  Len         int
}

func (e PasswordError) Error() string {
  return "Password is too short."
}

func RegisterAccount(name, password string) error {
  if len(password) < 8 {
    return PasswordError{RequiredLen: 8, Len: len(password)}
  }
  // register account logic
  // ...
  return nil
}

func main() {
  err := RegisterAccount("john", "12345")
  if err != nil {
    if errInfo, ok := err.(PasswordError); ok {
      fmt.Printf("%v (Required: %d Len: %d)\n", errInfo, errInfo.RequiredLen, errInfo.Len)
    }
  }
}

When you execute the code, you can see the result like the below.

# go run main.go
Password is too short. (Required: 8 Len: 5)

Like this, if you want to add more information to the error, you can use the error type.

Error wrapping

In Golang, you can wrap the various errors to make one error. To wrap errors, you can use the fmt.Errorf function.

fmt.Errorf("%w", err)

Also, you can use the Is and As function in the errors package to check the error is wrapped or not.

errors.Is(err, StrError)
errors.As(err, &strError)

To check this, modify the main.go file like the below.

package main

import (
  "errors"
  "fmt"
)

type StrError struct {
  Msg string
}

func (e *StrError) Error() string {
  return fmt.Sprintf("Message: %s", e.Msg)
}

func msgError(msg string) error {
  return &StrError{Msg: msg}
}

func WrapError(msg string) error {
  err := msgError(msg)
  return fmt.Errorf("(Wrapping) %w", err)
}

func main() {
  err1 := msgError("Error")
  fmt.Println("[Normal Error]", err1)

  err2 := WrapError("Test Error Message")
  fmt.Println("[Wrapping Error]", err2)

  var strErr *StrError
  if errors.As(err2, &strErr) {
    fmt.Printf("[Failed] %s\n", strErr)
  }
}

When you execute the code, you can see the following result.

# go run main.go
[Normal Error] Message: Error
[Wrapping Error] (Wrapping) Message: Test Error Message
[Failed] Message: Test Error Message

Like this, you can wrap the errors to make one error and handle it in Golang.

Panic

In Golang, you can use the panic to exit the program with the error. Panic makes the program exit quickly, so makes the programmers recognize it and handle it faster.

To check this, modify the main.go file like the below.

package main

import "fmt"

func divide(a, b int) {
  if b == 0 {
    panic("divide by zero")
  }
  fmt.Printf("%d / %d = %d\n", a, b, a/b)
}

func main() {
  divide(4, 2)
  divide(4, 0)
  divide(4, 3)
}

Whe the code is executed, you can see the result like the below.

# go run main.go
4 / 2 = 2
panic: divide by zero

goroutine 1 [running]:
main.divide(0x4, 0x2)
        /study-golang/error-handling/5.panic/main.go:7 +0x10b
main.main()
        /study-golang/error-handling/5.panic/main.go:14 +0x31
exit status 2

The panic occurs in the second divide function call. So the program exits with the error, and the third divide function call is not executed.

You can pass any types to the panic function like the below.

func panic(v interface{})

So, you can use the panic as follows.

panic(42)
panic("error")
panic(fmt.Errorf("error num: %d", 42))
panic(SomeType{SomeData})

Panic propagation and recovery

When we develop the programs, it’s important to find the issue fastly. So, when we use the panic, it helps us to find the issue quickly and fix it.

However, when the program is released, it’s important to keep the service continuously running. When people use the program and the program is crashed fastly, no one wants to use the program.

So, Golang provides the recover to recover the panic.

func recover() interface {}

We can recover the panic with the recover and defer in the funciton. To check this, modify the main.go file like the below.

package main

import (
  "fmt"
)

func firstCall() {
  fmt.Println("(2) firstCall function start")
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered in firstCall", r)
    }
  }()

  group()
  fmt.Println("(2) firstCall function end")
}

func group() {
  fmt.Println("(3) group function start")
  fmt.Printf("4/2 = %d\n", divide(4, 2))
  fmt.Printf("4/0 = %d\n", divide(4, 0))
  fmt.Println("(3) group function end")
}

func divide(a, b int) int {
  if b == 0 {
    panic("divide by zero")
  }
  return a / b
}

func main() {
  fmt.Println("(1) main function start")
  firstCall()
  fmt.Println("(1) main function end")
}

When you execute the code, you can see the result like the below.

# go run main.go
(1) main function start
(2) firstCall function start
(3) group function start
4/2 = 2
Recovered in firstCall divide by zero
(1) main function end

As you see, the recover is used in the firstCall function to recover the panic. And the panic ourrs in the divide function. So, the panic is propagated to divide() > group() > firstCall() > main(). In here, we recovered the panic in the firstCall function, so the panic is not propagated to the main().

When you’re developing, I recommend you not to use the recover. If you use the recover, the program won’t exit, so it’s difficult to find the error or recognize it.

When the program is released, we don’t have any choice. We should use the recover to keep the service running. However, the same problem occurs. The recover makes us hard to find the issues, so making the email notificaion or saving the error to the database may be a good solution for this.

Golang does not support SHE

Golang doesn’t support SHE(Structured Exception Handling).

try {
  ...
} catch (Exception ex) {
  ...
} finally {
  ...
}

Other programmin languages supoort the try-catch statement to handle errors, but Golang doesn’t support it. SHE has some issues as follows.

  1. Performance issue: the structure is cheked and executed in the normal status, so it makes some performance issue.
  2. It is difficult to recognize errors. For this reason, some errors are not handled and may be neglected.

Completed

Done! we’ve seen how to define errors and how to handle them in Goalng. We’ve seen how to make an error with the program running and how to make the program exit with an error by panic. Also we’ve seen how to use the recover to recover the error.

The error is very important role in the programming. So, if you use the function which returns an error, it’s better to handle it instead of using the blank identifier(_). Also, using the recover to recover the panic for keeping the program running is a good solution, but it’s better to send a notification or save the error to the database for recoginizing the error.

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

Posts