Page Content

Tutorials

Error Handling In GoLang: Chaining Errors for Better Debug

Error Handling In GoLang

Go’s error-handling features, which are very different from exception-based systems in other languages, are essential to creating software that is dependable and strong. Go encourages developers to check and handle problems openly by highlighting the fact that they are values that functions return like any other value.

Built-in error type and Basic Error Creation

Error() is the only method in the built-in error type of Go, which is an interface that returns a string:

type error interface {
    Error() string
}

By merely using this technique, every type can be turned into an error with this design.

New error values can be generated by using:

  • errors.New: This function, which comes from the errors standard package, returns a new error after receiving a string.
  • The fmt package automatically retrieves the string representation by calling the err variable’s Error() function.
  • fmt.Errorf: This function lets you write error messages that are dynamically formatted, like fmt.Printf.
  • By using changeable data, this enables more informative error messages.

Handling Errors

To deal with problems in Go, the most popular and idiomatic method is to see if the returned error value is not zero. There was no error, as shown by a nil value.

In addition to the result, functions frequently return a possible error. The last return value is typically the error.

package main
import (
    "errors"
    "fmt"
    "strings"
)
func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil // Returns nil for the error if successful
}
func main() {
    // Call function and capture both return values
    name, err := capitalize("govindhtech")
    if err != nil { // Check if an error occurred
        fmt.Println("Could not capitalize:", err) 
        return // Exit if error
    }
    fmt.Println("Capitalized name:", name)
}

Output

Capitalized name: GOVINDHTECH

If the error is all that matters to you, you can reject other return values by using the blank identifier (_):

package main
import (
    "errors"
    "fmt"
    "strings"
)
func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}
func main() {
    _, err := capitalize("") // Discard the string value
    if err != nil {
        fmt.Println("Could not capitalize:", err) 
        return
    }
    fmt.Println("Success!")
}

Output

Could not capitalize: no name provided

In order to streamline function calls and error handling, Go additionally provides an optional assignment clause in if statements:

package main
import (
    "errors"
    "fmt"
)
func boom() error {
    return errors.New("barnacles")
}
func main() {
    if err := boom(); err != nil { // Assigns and checks in one line
        fmt.Println("An error occurred:", err) 
        return
    }
    fmt.Println("Anchors away!")
}

Output

An error occurred: barnacles

Error handling (often known as the “sad path”) is kept at the second indent level, while the “happy path” logic remains at the first.

Custom Error Types and Type Assertions

It is possible to define your own struct that implements the error interface for more intricate error information. You can now attach more fields than just a string message with this.

Example of a custom error type:

package main
import (
    "fmt"
    "errors"
    "os"
)
type RequestError struct {
    StatusCode int
    Err        error
}
func (r *RequestError) Error() string {
    return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
}
func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}
func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err) 
        os.Exit(1)
    }
    fmt.Println("success!")
}

Output

status 503: err unavailable

RequestError in this case contains both an underlying issue and a status code. Error() converts these details into a single string when it is called.

A type assertion is used to access the particular properties of a custom error type, as the error interface only exposes Error():

package main
import (
    "errors"
    "fmt"
    "net/http"
    "os"
)
type RequestError struct {
    StatusCode int
    Err        error
}
func (r *RequestError) Error() string {
    return r.Err.Error()
}
func (r *RequestError) Temporary() bool {
    return r.StatusCode == http.StatusServiceUnavailable // 503
}
func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}
func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err) 
        // Attempt a type assertion to the concrete RequestError type
        if re, ok := err.(*RequestError); ok {
            if re.Temporary() {
                fmt.Println("This request can be tried again") 
            } else {
                fmt.Println("This request cannot be tried again")
            }
        }
        os.Exit(1)
    }
}

Output

unavailable
This request can be tried again
exit status 1

Converting the err interface to a (*RequestError) is the goal of the re, ok := err.(*RequestError) syntax. If the conversion is successful, the Temporary() method will be accessible as ok will be true.

Wrapping Errors: In order to give debugging context, errors can also be wrapped. One mistake sets the stage for another, resulting in a series of mistakes.

package main
import (
    "errors"
    "fmt"
)
type WrappedError struct {
    Context string
    Err     error
}
func (w *WrappedError) Error() string {
    return fmt.Sprintf("%s: %v", w.Context, w.Err)
}
func Wrap(err error, info string) *WrappedError {
    return &WrappedError{
        Context: info,
        Err:     err,
    }
}
func main() {
    err := errors.New("boom!")
    err = Wrap(err, "main")
    fmt.Println(err) 
}

Output

main: boom!

This lets you give an error context as it moves through your application’s levels.

defer statement

After the surrounding code has finished running, the defer statement plans to execute a function call. Because it maintains the cleanup code close to where the resource was accessed, it is especially helpful for resource cleanup (such as shutting down files or network connections) and guarantees that it continues to run even in the event of a panic.

package main
import (
    "io"
    "log"
    "os"
)
func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}
func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    // Defer the file.Close() call. It will run when 'write' function returns.
    defer file.Close()
    _, err = io.WriteString(file, text)
    if err != nil {
        return err // file.Close() will still be called here due to defer
    }
    // Optionally, return file.Close() here to report its error specifically.
    // The deferred file.Close() will still run afterwards but its error will be ignored.
    return file.Close()
}

Output

This is a readme file

In this instance, defer the file.It is ensured via Close().Whether io is used to invoke Close().Whether WriteString works or not, resource leaks are avoided.

panic and recover

Although Go promotes the use of explicit error handling, it also offers panic and recovery for unforeseen or unusual situations.

panic: Immediately stopping the current function’s execution, this built-in function starts “panicking.” It performs any deferred operations as it unwinds the call stack. Program termination occurs if no function in the call stack recovers. Generally speaking, panics signify a programming fault (such as accessing an out-of-bounds array index or nil pointer dereference) or a situation from which there is no simple solution.

recover: The value supplied to panic is returned by this built-in method, which also ends a panicking goroutine. It only works when invoked within a deferred function.

In this instance, panic is triggered while dividing by zero. Nevertheless, the deferred function logs the panic, uses recover() to catch it, and permits the application to continue running rather than crashing. Recovery will return nil in this scenario, making panic indistinguishable from a non-panicked state, hence it is imperative to avoid passing nil to panic.

In conclusion, Go’s error handling philosophy emphasizes explicit checks and unambiguous notification of predicted failures through error values. Defer is used for cleanup, while panic/recover is used for circumstances that are truly unexpected and unrecoverable.

Agarapu Geetha
Agarapu Geetha
My name is Agarapu Geetha, a B.Com graduate with a strong passion for technology and innovation. I work as a content writer at Govindhtech, where I dedicate myself to exploring and publishing the latest updates in the world of tech.
Index