Quick Summary:
Golang Error Handling has been the talk of the town because of its unconventional approach, unlike other languages that utilize try…catch block. It was quite difficult for the developers to digest the new process of Golang error handling patterns.
Go’s way of error handling has also been questioned and criticized as it was entirely out of the box. But, after few months of frustration, the technique of Golang error best practices proved to be remarkable. In this blog, I’ll discuss the basics of Golang Error Handling with examples and why is it having a better approach than other languages. For simplifying the blog, I’ve classified it into sections.
Before moving on to Golang Error Handling’s technique, I would like to discuss a bit about Error and Error handling.
Errors are defined as the unwanted and unusual conditions encountered in the program. It can be either compile time or run time. Various examples are – accessing a file that does not exist, a failed db connection, or abnormal user inputs. Anything could generate an error.
Now the process for predicting where your program could behave abnormally and the technique of implementing the solution for further diagnosis is Error Handling. You might be familiar try…catch block for handling errors in Java, PHP, or Python.
Now let’s start with Error handling in Golang.
Exploring Golang Error Handling
Getting familiar with new approaches has always been difficult, no matter how clean and straightforward it can be. And, when people get frustrated with such new methods, they start criticizing them. That’s what happened with Go. Developers were dealing with conventional techniques; thus, it was quite challenging for them to make room for Go’s way of error handling. Many proposals were made to change and improve Golang Error Handling patterns, as you can see in this image taken from github.
There’s a lot to learn about the methods of Go error handling but before that, I would like to discuss the built-in error type of Golang.
The Error Type
If you’ve ever coded in Go you would be quite familiar with the error type. Now, the question might arise what is this error type?
The error type is nothing but a type of an interface. It is the type given to the error variable which declares itself as a string.
The syntax looks something like this-
type error interface { Error() string }
Exploring Golang Error Handling Patterns
Golang’s philosophy behind error handling is very straightforward – Don’t overlook errors; they are critically important. The syntax of func f() (value, error) is quite a piece of cake to learn and implement, even for those who have just started with Go.
Golang Error Handling techniques implicitly force the developers to use errors as first-class values of the functions. In case you have missed returning the error string from your function like this –
func getUsers() (*Users, error) { .... } func main() { users, _ := getUsers() }
Almost all the IDEs and linters will notice that you’ve missed returning the error and will make it salient for your fellow developers while reviewing the code. In this way, Golang doesn’t force you to use error as a first-class value of your function ‘explicitly,’ but neither does it allow you to overlook it.
Golang just provides a built-in error type due to which you don’t forget how critical these errors can be. If you choose not to fire any actions when the program encounters an error because of err != nil, you have to be prepared for the consequences; even Golang would be unable to save you! Let’s have one example of Golang error handling best practices.
if error := criticalOperation(); error != nil { // Not returning anything is a bad practice. log.Printf("Oops! Something went wrong in the program", error) // `return` your error message hereafter this line! } if error := saveData(data); error != nil { return fmt.Errorf("Data has been lost", error) }
When err != nil is encountered while calling criticalOperation() and if you choose to log the error message instead of handling it intelligently, even Go won’t save your program from the errors. Golang just provides you how to return and use the errors; further, Error handling in golang handling the Go errors is entirely up to you.
Golang prefers to use the panic and recover method rather than throwing exceptions and using try…catch block. We will learn more about that later. I hope you now had a basic idea of Go error handling. Now, let’s see why the Error handling in golang is better than other languages. And for that, we need to learn a bit about how different languages handle their errors.
Do you need assistance to solve your Golang error?
Hire Golang developer from us to fix the bugs and fine-tune your Golang app user experience.
Throwing Exception: Error Handling Way of Other Programming Languages
Those developers familiar with Javascript frameworks, Java, Python, Ruby, and PHP, might better understand how these languages handle their errors. Look at this code snippet of how to throw an exception-
try { criticalDataOperation1(); criticalDataOperation2(); criticalDataOperation3(); } catch (err) { console.error(err); }
While executing the function criticalDataOperations(), it will jump to the catch block if an error occurs, and console.log(err) will be performed. The function criticalOperations() doesn’t have to explicitly state the flow of error, for which it will jump the catch block. If any exception is thrown while executing these functions, then the program will directly log the error. And this is the advantage of exception-based programs: if you have forgotten to handle some exceptions, then also the stack trace will notice it at the run time and move forward to catch block.
Throwing exceptions is not the only way of error handling; Rust is also one of its types. Rust provides good pattern matching with simple syntax to search errors and acquire similar results like exceptions.
Isn’t it strange to digest why Golang didn’t utilize exceptions, a conventional way of error handling, and came up with such a unique approach? Let’s quench our curiosity and dive for the answer.
Why didn’t Golang utilize exceptions, a conventional way to handle errors?
Two key points that are kept in mind while Golang error handling is:
- Keep it simple.
- Plan where it can go wrong.
Golang tends to keep the syntax and usage of if err != nil as simple as possible. It returns (value, err) from the function to ensure how to handle the program’s failure. You don’t need to stress yourself with the complications of nested try…catch blocks. The practice of exception-based code never lets the developers search the actual errors; instead, they will throw the exception, which will be handled in the catch block.
Developers are forced to analyze every situation in exception-based languages and throw exceptions without adequately addressing them. Whereas, Golang return error handle your errors and return them as values from the functions.
Here are the advantages of Golang new error handling.
- Transparent control-flow.
- No interruption of sudden uncaught exceptions.
- You have full control over the errors as they are considered as values – you can do whatever with that.
- Simple syntax.
- Easy implementation of error chains to take action on the error.
The last point might seem quite confusing to you. Let me make it simpler for you. The easy syntax of if err != nil allows you to chain the functions returning errors throughout the hierarchy of your program until you have reached the actual error, which has to be handled precisely. The practice of chaining the errors can be relatively easy to traverse and debug, even for your teammates.
Here is the example for error-chaining.
// controllers/users.go if error := db.CreateUserforDB(user); error != nil { return fmt.Errorf("error while creating user: %w", error) } // database/users.go func (db *Database) CreateUserforDB(user *User) error { ok, error := db.DoesUserExistinDB(user) if error != nil { return fmt.Errorf("error in db while checking: %w", err) } ... } func (db *Database) DoesUserExistinDB(user *User) error { if error := db.Connected(); error != nil { return fmt.Errorf("error while establishing connection: %w", err) } ... } func (db *Database) Connected() error { if !isInternetConnectionEstablished() { return errors.New("not connected to internet") } ... }
The advantage of the above code is that every block has returned informative errors that can be easily understood and are responsible for those errors they are aware of. This kind of Golang handle error chaining helps your program not to break unexpectedly and makes traversing of errors less time taking. You can also choose to use the stack trace in your function and utilize this library for exploring various built-in functions.
So far, we have seen Golang Error Handling best practices and fundamental way of using if…err != nil. Do you remember I have used panic and recover before, let’s see what the fuss is about?
Golang Error Handling: Panic and Recover Mechanism
As I have mentioned before, Golang has panic and recover rather than try and catch blocks. You might have seen try…catch block so many times in the program, so I believe the exception handling is not so exceptionally handled – what an irony! Sometimes, developers use exception handling to throw a custom error message; this usage complicates runtime errors and custom errors (avoid such practices).
Whereas Golang has a different method for custom errors, we have learned so far, i.e., of throwing Golang a custom error message by returning the error as the function’s value. And panic and recover technique is used in exceptional cases only, unlike try and catch.
If there’s a truly exceptional case for which you have to use a panic scenario; it will stop the regular function’s flow and start panicking. When function func has called panic(), the func won’t be executed further though other deferred functions will be performed as expected.
Recover is the built-in function that frees the function from its panicking state. It is only used inside the deferred functions. While executing the function normally, recover will return nil without any other effects.
Here is a simple code snippet for better understanding.
Panicking
exceptionalCondition := true if exceptionalCondition { panic("panicking!!") }
Creating panic in the programs is more manageable than handling it.
Recover: To Rescue From Panic
func F() { defer func() { if error := recover(); error != nil { fmt.Println("This is the error: ", err) }() //do whatever here... }
You can add an anonymous function or make a custom function using defer keyword.
This was a high overview of what is panic and recover mechanism and how does it work.
Conclusion
Thus, this was all about Golang Error Handling basics, how it is better than languages, and a high overview of panic and recover mechanism. I hope that this blog has helped you the way you have expected it. Being a globally renowned Golang development company, we have established our reputation in providing best-in-class services to cater to your golang project requirements. Hire Golang developer from us and turn your idea into a reality that is suited to your business needs.
In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so.
Error handling in Golang is unconventional when compared to other mainstream languages like Javascript, Java and Python. This can make it very difficult for new programmers to grasp Golangs approach of tackling error handling.
In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so. A basic understanding of Golang is therefore required to follow this article. If you are unsure about any concepts, you can look them up here.
Errors in Golang
Errors indicate an unwanted condition occurring in your application. Let’s say you want to create a temporary directory where you can store some files for your application, but the directory’s creation fails. This is an unwanted condition and is therefore represented using an error.
package main
import (
"fmt"
"ioutil"
)
func main() {
dir, err := ioutil.TempDir("", "temp")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}
}
Golang represents errors using the built-in error type, which we will look at closer in the next section. The error is often returned as a second argument of the function, as shown in the example above. Here the TempDir function returns the name of the directory as well as an error variable.
Creating custom errors
As already mentioned errors are represented using the built-in error interface type, which has the following definition:
type error interface {
Error() string
}
The interface contains a single method Error() that returns an error message as a string. Every type that implements the error interface can be used as an error. When printing the error using methods like fmt.Println the Error() method is automatically called by Golang.
There are multiple ways of creating custom error messages in Golang, each with its own advantages and disadvantages.
String-based errors
String-based errors can be created using two out-of-the-box options in Golang and are used for simple errors that just need to return an error message.
err := errors.New("math: divided by zero")
The errors.New() method can be used to create new errors and takes the error message as its only parameter.
err2 := fmt.Errorf("math: %g cannot be divided by zero", x)
fmt.Errorf on the other hand also provides the ability to add formatting to your error message. Above you can see that a parameter can be passed which will be included in the error message.
Custom error with data
You can create your own error type by implementing the Error() function defined in the error interface on your struct. Here is an example:
type PathError struct {
Path string
}
func (e *PathError) Error() string {
return fmt.Sprintf("error in path: %v", e.Path)
}
The PathError implements the Error() function and therefore satisfies the error interface. The implementation of the Error() function now returns a string with the path of the PathError struct. You can now use PathError whenever you want to throw an error.
Here is an elementary example:
package main
import(
"fmt"
)
type PathError struct {
Path string
}
func (e *PathError) Error() string {
return fmt.Sprintf("error in path: %v", e.Path)
}
func throwError() error {
return &PathError{Path: "/test"}
}
func main() {
err := throwError()
if err != nil {
fmt.Println(err)
}
}
You can also check if the error has a specific type using either an if or switch statement:
if err != nil {
switch e := err.(type) {
case *PathError :
// Do something with the path
default:
log.Println(e)
}
}
This will allow you to extract more information from your errors because you can then call all functions that are implemented on the specific error type. For example, if the PathError had a second method called GetInfo you could call it like this.
e.GetInfo()
Error handling in functions
Now that you know how to create your own custom errors and extract as much information as possible from errors let’s take a look at how you can handle errors in functions.
Most of the time errors are not directly handled in functions but are returned as a return value instead. Here we can take advantage of the fact that Golang supports multiple return values for a function. Thus you can return your error alongside the normal result — errors are always returned as the last argument — of the function as follows:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0.0, errors.New("cannot divide through zero")
}
return a/b, nil
}
The function call will then look similar to this:
func main() {
num, err := divide(100, 0)
if err != nil {
fmt.Printf("error: %s", err.Error())
} else {
fmt.Println("Number: ", num)
}
}
If the returned error is not nil it usually means that there is a problem and you need to handle the error appropriately. This can mean that you use some kind of log message to warn the user, retry the function until it works or close the application entirely depending on the situation. The only drawback is that Golang does not enforce handling the retuned errors, which means that you could just ignore handling errors completely.
Take the following code for example:
package main
import (
"errors"
"fmt"
)
func main() {
num2, _ := divide(100, 0)
fmt.Println("Number: ", num2)
}
The so-called blank identifier is used as an anonymous placeholder and therefore provides a way to ignore values in an assignment and avoid compiler errors in the process. But remember that using the blank identifier instead of probably handling errors is dangerous and should not be done if it can be avoided.
Defer, panic and recover
Go does not have exceptions like many other programming languages, including Java and Javascript but has a comparable mechanism know as ,,Defer, panic and recover». Still the use-cases of panic and recover are very different from exceptions in other programming languages as they should only be used in unexpected and unrecoverable situations.
Defer
A defer statement is a mechanism used to defer a function by putting it into an executed stack once the function that contains the defer statement has finished, either normally by executing a return statement or abnormally panicking. Deferred functions will then be executed in reverse order in which they were deferred.
Take the following function for example:
func processHTML(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
resp.Body.Close()
return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
// ... Process HTML ...
return nil
}
Here you can notice the duplicated resp.Body.Close call, which ensures that the response is properly closed. Once functions grow more complex and have more errors that need to be handled such duplications get more and more problematic to maintain.
Since deferred calls get called once the function has ended, no matter if it succeeded or not it can be used to simplify such calls.
func processHTMLDefer(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
}
doc, err := html.Parse(resp.Body)
// ... Process HTML ...
return nil
}
All deferred functions are executed in reverse order in which they were deferred when the function finishes.
package main
import (
"fmt"
)
func main() {
first()
}
func first() {
defer fmt.Println("first")
second()
}
func second() {
defer fmt.Println("second")
third()
}
func third() {
defer fmt.Println("third")
}
Here is the result of running the above program:
third
second
first
Panic
A panic statement signals Golang that your code cannot solve the current problem and it therefore stops the normal execution flow of your code. Once a panic is called, all deferred functions are executed and the program crashes with a log message that includes the panic values (usually an error message) and a stack trace.
As an example Golang will panic when a number is divided by zero.
package main
import "fmt"
func main() {
divide(5)
}
func divide(x int) {
fmt.Printf("divide(%d) n", x+0/x)
divide(x-1)
}
Once the divide function is called using zero, the program will panic, resulting in the following output.
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2
You can also use the built-in panic function to panic in your own programms. A panic should mostly only be used when something happens that the program didn’t expect and cannot handle.
func getArguments() {
if len(os.Args) == 1 {
panic("Not enough arguments!")
}
}
As already mentioned, deferred functions will be executed before terminating the application, as shown in the following example.
package main
import (
"fmt"
)
func main() {
accessSlice([]int{1,2,5,6,7,8}, 0)
}
func accessSlice(slice []int, index int) {
fmt.Printf("item %d, value %d n", index, slice[index])
defer fmt.Printf("defer %d n", index)
accessSlice(slice, index+1)
}
Here is the output of the programm:
item 0, value 1
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
defer 5
defer 4
defer 3
defer 2
defer 1
defer 0
panic: runtime error: index out of range [6] with length 6
goroutine 1 [running]:
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x6)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:29 +0x250
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:9 +0x99
exit status 2
Recover
In some rare cases panics should not terminate the application but be recovered instead. For example, a socket server that encounters an unexpected problem could report the error to the clients and then close all connections rather than leaving the clients wondering what just happened.
Panics can therefore be recovered by calling the built-in recover function within a deferred function in the function that is panicking. Recover will then end the current state of panic and return the panic error value.
package main
import "fmt"
func main(){
accessSlice([]int{1,2,5,6,7,8}, 0)
}
func accessSlice(slice []int, index int) {
defer func() {
if p := recover(); p != nil {
fmt.Printf("internal error: %v", p)
}
}()
fmt.Printf("item %d, value %d n", index, slice[index])
defer fmt.Printf("defer %d n", index)
accessSlice(slice, index+1)
}
As you can see after adding a recover function to the function we coded above the program doesn’t exit anymore when the index is out of bounds by recovers instead.
Output:
item 0, value 1
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
internal error: runtime error: index out of range [6] with length 6defer 5
defer 4
defer 3
defer 2
defer 1
defer 0
Recovering from panics can be useful in some cases, but as a general rule you should try to avoid recovering from panics.
Error wrapping
Golang also allows errors to wrap other errors which provides the functionality to provide additional context to your error messages. This is often used to provide specific information like where the error originated in your program.
You can create wrapped errors by using the %w flag with the fmt.Errorf function as shown in the following example.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Printf("error running program: %s n", err.Error())
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
The output of the application would now look like the following:
error running program: error opening non-existing: open non-existing: no such file or directory
As you can see the application prints both the new error created using fmt.Errorf as well as the old error message that was passed to the %w flag. Golang also provides the functionality to get the old error message back by unwrapping the error using errors.Unwrap.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Printf("error running program: %s n", err.Error())
// Unwrap error
unwrappedErr := errors.Unwrap(err)
fmt.Printf("unwrapped error: %v n", unwrappedErr)
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
As you can see the output now also displays the original error.
error running program: error opening non-existing: open non-existing: no such file or directory
unwrapped error: open non-existing: no such file or directory
Errors can be wrapped and unwrapped multiple times, but in most cases wrapping them more than a few times does not make sense.
Casting Errors
Sometimes you will need a way to cast between different error types to for example, access unique information that only that type has. The errors.As function provides an easy and safe way to do so by looking for the first error in the error chain that fits the requirements of the error type. If no match is found the function returns false.
Let’s look at the official errors.As docs example to better understand what is happening.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main(){
// Casting error
if _, err := os.Open("non-existing"); err != nil {
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
Here we try to cast our generic error type to os.PathError so we can access the Path variable that that specific error contains.
Another useful functionality is checking if an error has a specific type. Golang provides the errors.Is function to do exactly that. Here you provide your error as well as the particular error type you want to check. If the error matches the specific type the function will return true, if not it will return false.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main(){
// Check if error is a specific type
if _, err := os.Open("non-existing"); err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")
} else {
fmt.Println(err)
}
}
}
After checking, you can adapt your error message accordingly.
Sources
- Golang Blog — Working with Errors in Go 1.13
- The Go Programming language book
- Golang Blog — Defer, Panic, and Recover
- LogRocket — Error handling in Golang
- GolangByExample — Wrapping and Un-wrapping of error in Go
- Golang Documentation — Package errors
Conclusion
You made it all the way until the end! I hope this article helped you understand the basics of Go error handling and why it is an essential topic in application/software development.
If you have found this helpful, please consider recommending and sharing it with other fellow developers and subscribing to my newsletter. If you have any questions or feedback, let me know using my contact form or contact me on Twitter.
Содержание
- Go Best Practices — Error handling
- Some background — The error interface
- Checking for errors
- Using error structs and type checking
- Using the errors package and direct comparison
- Immediate error handling
- That’s all for today
- Golang Error Handling: Go’s Way of Handling Errors
- TABLE OF CONTENTS
- Subscribe for weekly updates
- What are Error and Error Handling?
- Exploring Golang Error Handling
- The Error Type
- Exploring Golang Error Handling Patterns
- Throwing Exception: Error Handling Way of Other Programming Languages
- Golang Error Handling: Panic and Recover Mechanism
- Panicking
- Recover: To Rescue From Panic
- Conclusion
- Error handling in Golang
- Errors in Golang
- Creating custom errors
- String-based errors
- Custom error with data
- Error handling in functions
- Defer, panic and recover
- Defer
- Panic
- Recover
- Error wrapping
- Casting Errors
- Sources
- Conclusion
Go Best Practices — Error handling
This is the first article in a series of lessons I’ve learnt over the couple years I’ve worked with Go in production. We are running a good number of Go services in production at Saltside Technologies (psst, I’m hiring for multiple positions in Bangalore for Saltside) and I’m also running my own business where Go is an integral part.
We will cover a broad range of subjects, large and small.
The first subject I wanted to cover in this series is error handling. It’s often causing confusion and annoyance for new Go developers.
Some background — The error interface
Just so we’re on the same page. As you may know an error in Go is simply anything that implements the error interface. This is what the interface definition looks like:
So anything that implements the Error() string method can be used as an error.
Checking for errors
Using error structs and type checking
When I started writing Go I often did string comparisons of error messages to see what the error type was (yes, embarrassing to think of but sometimes you need to look back to go forward).
A better approach is to use error types. So you can (of course) create structs that implements the error interface and then do type comparison in a switch statement.
Here’s an example error implementation.
Now this error can be used like this.
Here’s the Go Play link for the full example. Notice the switch err.(type) pattern, which makes it possible to check for different error types rather than something else (like string comparison or something similar).
Using the errors package and direct comparison
The above approach can alternatively be handled using the errors package. This approach is recommendable for error checks within the package where you need a quick error representation.
This approach is less good when you need more complex error objects with e.g. error codes etc. In that case you should create your own type that implements the error interface.
Sometimes I come across code like the below (but usually with more fluff around..):
The point here is that the err is not handled immediately. This is a fragile approach since someone can insert code between err := call1() and the return err , which would break the intent, since that may shadow the first error. Two alternate approaches:
Both of the above approaches is fine with me. They achieve the same thing, which is; if someone needs to add something after call1() they need to take care of the error handling.
That’s all for today
Stay tuned for the next article about Go Best Practices. Go strong :).
Источник
Golang Error Handling: Go’s Way of Handling Errors
Last Updated on December 27, 2022
TABLE OF CONTENTS
Subscribe for
weekly updates
Quick Summary:
Golang Error Handling has been the talk of the town because of its unconventional approach, unlike other languages that utilize try…catch block. It was quite difficult for the developers to digest the new process of Golang error handling patterns.
Go’s way of error handling has also been questioned and criticized as it was entirely out of the box. But, after few months of frustration, the technique of Golang error best practices proved to be remarkable. In this blog, I’ll discuss the basics of Golang Error Handling with examples and why is it having a better approach than other languages. For simplifying the blog, I’ve classified it into sections.
What are Error and Error Handling?
Before moving on to Golang Error Handling’s technique, I would like to discuss a bit about Error and Error handling.
Errors are defined as the unwanted and unusual conditions encountered in the program. It can be either compile time or run time. Various examples are – accessing a file that does not exist, a failed db connection, or abnormal user inputs. Anything could generate an error.
Now the process for predicting where your program could behave abnormally and the technique of implementing the solution for further diagnosis is Error Handling. You might be familiar try…catch block for handling errors in Java, PHP, or Python.
Now let’s start with Error handling in Golang.
Exploring Golang Error Handling
Getting familiar with new approaches has always been difficult, no matter how clean and straightforward it can be. And, when people get frustrated with such new methods, they start criticizing them. That’s what happened with Go. Developers were dealing with conventional techniques; thus, it was quite challenging for them to make room for Go’s way of error handling. Many proposals were made to change and improve Golang Error Handling patterns, as you can see in this image taken from github.
There’s a lot to learn about the methods of Go error handling but before that, I would like to discuss the built-in error type of Golang.
The Error Type
If you’ve ever coded in Go you would be quite familiar with the error type. Now, the question might arise what is this error type?
The error type is nothing but a type of an interface. It is the type given to the error variable which declares itself as a string.
The syntax looks something like this-
Exploring Golang Error Handling Patterns
Golang’s philosophy behind error handling is very straightforward – Don’t overlook errors; they are critically important. The syntax of func f() (value, error) is quite a piece of cake to learn and implement, even for those who have just started with Go.
Golang Error Handling techniques implicitly force the developers to use errors as first-class values of the functions. In case you have missed returning the error string from your function like this –
Almost all the IDEs and linters will notice that you’ve missed returning the error and will make it salient for your fellow developers while reviewing the code. In this way, Golang doesn’t force you to use error as a first-class value of your function ‘explicitly,’ but neither does it allow you to overlook it.
Golang just provides a built-in error type due to which you don’t forget how critical these errors can be. If you choose not to fire any actions when the program encounters an error because of err != nil, you have to be prepared for the consequences; even Golang would be unable to save you! Let’s have one example of Golang error handling best practices.
When err != nil is encountered while calling criticalOperation() and if you choose to log the error message instead of handling it intelligently, even Go won’t save your program from the errors. Golang just provides you how to return and use the errors; further, Error handling in golang handling the Go errors is entirely up to you.
Golang prefers to use the panic and recover method rather than throwing exceptions and using try…catch block. We will learn more about that later. I hope you now had a basic idea of Go error handling. Now, let’s see why the Error handling in golang is better than other languages. And for that, we need to learn a bit about how different languages handle their errors.
Do you need assistance to solve your Golang error?
Hire Golang developer from us to fix the bugs and fine-tune your Golang app user experience.
Throwing Exception: Error Handling Way of Other Programming Languages
Those developers familiar with Javascript frameworks, Java, Python, Ruby, and PHP, might better understand how these languages handle their errors. Look at this code snippet of how to throw an exception-
While executing the function criticalDataOperations(), it will jump to the catch block if an error occurs, and console.log(err) will be performed. The function criticalOperations() doesn’t have to explicitly state the flow of error, for which it will jump the catch block. If any exception is thrown while executing these functions, then the program will directly log the error. And this is the advantage of exception-based programs: if you have forgotten to handle some exceptions, then also the stack trace will notice it at the run time and move forward to catch block.
Throwing exceptions is not the only way of error handling; Rust is also one of its types. Rust provides good pattern matching with simple syntax to search errors and acquire similar results like exceptions.
Isn’t it strange to digest why Golang didn’t utilize exceptions, a conventional way of error handling, and came up with such a unique approach? Let’s quench our curiosity and dive for the answer.
Why didn’t Golang utilize exceptions, a conventional way to handle errors?
Two key points that are kept in mind while Golang error handling is:
- Keep it simple.
- Plan where it can go wrong.
Golang tends to keep the syntax and usage of if err != nil as simple as possible. It returns (value, err) from the function to ensure how to handle the program’s failure. You don’t need to stress yourself with the complications of nested try…catch blocks. The practice of exception-based code never lets the developers search the actual errors; instead, they will throw the exception, which will be handled in the catch block.
Developers are forced to analyze every situation in exception-based languages and throw exceptions without adequately addressing them. Whereas, Golang return error handle your errors and return them as values from the functions.
Here are the advantages of Golang new error handling.
- Transparent control-flow.
- No interruption of sudden uncaught exceptions.
- You have full control over the errors as they are considered as values – you can do whatever with that.
- Simple syntax.
- Easy implementation of error chains to take action on the error.
The last point might seem quite confusing to you. Let me make it simpler for you. The easy syntax of if err != nil allows you to chain the functions returning errors throughout the hierarchy of your program until you have reached the actual error, which has to be handled precisely. The practice of chaining the errors can be relatively easy to traverse and debug, even for your teammates.
Here is the example for error-chaining.
The advantage of the above code is that every block has returned informative errors that can be easily understood and are responsible for those errors they are aware of. This kind of Golang handle error chaining helps your program not to break unexpectedly and makes traversing of errors less time taking. You can also choose to use the stack trace in your function and utilize this library for exploring various built-in functions.
So far, we have seen Golang Error Handling best practices and fundamental way of using if…err != nil. Do you remember I have used panic and recover before, let’s see what the fuss is about?
Golang Error Handling: Panic and Recover Mechanism
As I have mentioned before, Golang has panic and recover rather than try and catch blocks. You might have seen try…catch block so many times in the program, so I believe the exception handling is not so exceptionally handled – what an irony! Sometimes, developers use exception handling to throw a custom error message; this usage complicates runtime errors and custom errors (avoid such practices).
Whereas Golang has a different method for custom errors, we have learned so far, i.e., of throwing Golang a custom error message by returning the error as the function’s value. And panic and recover technique is used in exceptional cases only, unlike try and catch.
If there’s a truly exceptional case for which you have to use a panic scenario; it will stop the regular function’s flow and start panicking. When function func has called panic(), the func won’t be executed further though other deferred functions will be performed as expected.
Recover is the built-in function that frees the function from its panicking state. It is only used inside the deferred functions. While executing the function normally, recover will return nil without any other effects.
Here is a simple code snippet for better understanding.
Panicking
Creating panic in the programs is more manageable than handling it.
Recover: To Rescue From Panic
You can add an anonymous function or make a custom function using defer keyword.
This was a high overview of what is panic and recover mechanism and how does it work.
Conclusion
Thus, this was all about Golang Error Handling basics, how it is better than languages, and a high overview of panic and recover mechanism. I hope that this blog has helped you the way you have expected it. Being a globally renowned Golang development company, we have established our reputation in providing best-in-class services to cater to your golang project requirements. Hire Golang developer from us and turn your idea into a reality that is suited to your business needs.
Источник
Error handling in Golang
In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so.
Error handling in Golang is unconventional when compared to other mainstream languages like Javascript, Java and Python. This can make it very difficult for new programmers to grasp Golangs approach of tackling error handling.
In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so. A basic understanding of Golang is therefore required to follow this article. If you are unsure about any concepts, you can look them up here.
Errors in Golang
Errors indicate an unwanted condition occurring in your application. Let’s say you want to create a temporary directory where you can store some files for your application, but the directory’s creation fails. This is an unwanted condition and is therefore represented using an error.
Golang represents errors using the built-in error type, which we will look at closer in the next section. The error is often returned as a second argument of the function, as shown in the example above. Here the TempDir function returns the name of the directory as well as an error variable.
Creating custom errors
As already mentioned errors are represented using the built-in error interface type, which has the following definition:
The interface contains a single method Error() that returns an error message as a string. Every type that implements the error interface can be used as an error. When printing the error using methods like fmt.Println the Error() method is automatically called by Golang.
There are multiple ways of creating custom error messages in Golang, each with its own advantages and disadvantages.
String-based errors
String-based errors can be created using two out-of-the-box options in Golang and are used for simple errors that just need to return an error message.
The errors.New() method can be used to create new errors and takes the error message as its only parameter.
fmt.Errorf on the other hand also provides the ability to add formatting to your error message. Above you can see that a parameter can be passed which will be included in the error message.
Custom error with data
You can create your own error type by implementing the Error() function defined in the error interface on your struct. Here is an example:
The PathError implements the Error() function and therefore satisfies the error interface. The implementation of the Error() function now returns a string with the path of the PathError struct. You can now use PathError whenever you want to throw an error.
Here is an elementary example:
You can also check if the error has a specific type using either an if or switch statement:
This will allow you to extract more information from your errors because you can then call all functions that are implemented on the specific error type. For example, if the PathError had a second method called GetInfo you could call it like this.
Error handling in functions
Now that you know how to create your own custom errors and extract as much information as possible from errors let’s take a look at how you can handle errors in functions.
Most of the time errors are not directly handled in functions but are returned as a return value instead. Here we can take advantage of the fact that Golang supports multiple return values for a function. Thus you can return your error alongside the normal result — errors are always returned as the last argument — of the function as follows:
The function call will then look similar to this:
If the returned error is not nil it usually means that there is a problem and you need to handle the error appropriately. This can mean that you use some kind of log message to warn the user, retry the function until it works or close the application entirely depending on the situation. The only drawback is that Golang does not enforce handling the retuned errors, which means that you could just ignore handling errors completely.
Take the following code for example:
The so-called blank identifier is used as an anonymous placeholder and therefore provides a way to ignore values in an assignment and avoid compiler errors in the process. But remember that using the blank identifier instead of probably handling errors is dangerous and should not be done if it can be avoided.
Defer, panic and recover
Go does not have exceptions like many other programming languages, including Java and Javascript but has a comparable mechanism know as ,,Defer, panic and recover». Still the use-cases of panic and recover are very different from exceptions in other programming languages as they should only be used in unexpected and unrecoverable situations.
Defer
A defer statement is a mechanism used to defer a function by putting it into an executed stack once the function that contains the defer statement has finished, either normally by executing a return statement or abnormally panicking. Deferred functions will then be executed in reverse order in which they were deferred.
Take the following function for example:
Here you can notice the duplicated resp.Body.Close call, which ensures that the response is properly closed. Once functions grow more complex and have more errors that need to be handled such duplications get more and more problematic to maintain.
Since deferred calls get called once the function has ended, no matter if it succeeded or not it can be used to simplify such calls.
All deferred functions are executed in reverse order in which they were deferred when the function finishes.
Here is the result of running the above program:
Panic
A panic statement signals Golang that your code cannot solve the current problem and it therefore stops the normal execution flow of your code. Once a panic is called, all deferred functions are executed and the program crashes with a log message that includes the panic values (usually an error message) and a stack trace.
As an example Golang will panic when a number is divided by zero.
Once the divide function is called using zero, the program will panic, resulting in the following output.
You can also use the built-in panic function to panic in your own programms. A panic should mostly only be used when something happens that the program didn’t expect and cannot handle.
As already mentioned, deferred functions will be executed before terminating the application, as shown in the following example.
Here is the output of the programm:
Recover
In some rare cases panics should not terminate the application but be recovered instead. For example, a socket server that encounters an unexpected problem could report the error to the clients and then close all connections rather than leaving the clients wondering what just happened.
Panics can therefore be recovered by calling the built-in recover function within a deferred function in the function that is panicking. Recover will then end the current state of panic and return the panic error value.
As you can see after adding a recover function to the function we coded above the program doesn’t exit anymore when the index is out of bounds by recovers instead.
Recovering from panics can be useful in some cases, but as a general rule you should try to avoid recovering from panics.
Error wrapping
Golang also allows errors to wrap other errors which provides the functionality to provide additional context to your error messages. This is often used to provide specific information like where the error originated in your program.
You can create wrapped errors by using the %w flag with the fmt.Errorf function as shown in the following example.
The output of the application would now look like the following:
As you can see the application prints both the new error created using fmt.Errorf as well as the old error message that was passed to the %w flag. Golang also provides the functionality to get the old error message back by unwrapping the error using errors.Unwrap.
As you can see the output now also displays the original error.
Errors can be wrapped and unwrapped multiple times, but in most cases wrapping them more than a few times does not make sense.
Casting Errors
Sometimes you will need a way to cast between different error types to for example, access unique information that only that type has. The errors.As function provides an easy and safe way to do so by looking for the first error in the error chain that fits the requirements of the error type. If no match is found the function returns false.
Let’s look at the official errors.As docs example to better understand what is happening.
Here we try to cast our generic error type to os.PathError so we can access the Path variable that that specific error contains.
Another useful functionality is checking if an error has a specific type. Golang provides the errors.Is function to do exactly that. Here you provide your error as well as the particular error type you want to check. If the error matches the specific type the function will return true, if not it will return false.
After checking, you can adapt your error message accordingly.
Sources
- Golang Blog — Working with Errors in Go 1.13
- The Go Programming language book
- Golang Blog — Defer, Panic, and Recover
- LogRocket — Error handling in Golang
- GolangByExample — Wrapping and Un-wrapping of error in Go
- Golang Documentation — Package errors
Conclusion
You made it all the way until the end! I hope this article helped you understand the basics of Go error handling and why it is an essential topic in application/software development.
If you have found this helpful, please consider recommending and sharing it with other fellow developers and subscribing to my newsletter. If you have any questions or feedback, let me know using my contact form or contact me on Twitter.
Источник
Introduction
This guide explains error handling in Go and the best practices for handling API errors in Go. You should have a working knowledge of Go and understand how web APIs work to get the most benefit from this article.
Go has a unique approach to handling errors. Rather than utilizing a try..catch block like other programming languages such as C++, Go treats errors as first-class values with panic and recovery mechanisms.
Go has a built-in type called error, which exposes an interface that implements the Error() method:
type error interface {
Error() string
}
Typically the usual way to handle errors in Go is to check if the returned error value is nil. If it’s equal to nil, then it means no errors occurred.
Go functions can also return multiple values. In cases where a function can fail, it’s a good idea to return the error status as a second return value. If the function doesn’t return anything, you should return the error status.
func myFunc() error {
// do something
}
err := myFunc()
if err != nil {
// do something
}
You can define errors using the errors.New() function, and have it printed out to the console using the Error() method:
func myFunc() error {
myErr := errors.New(My Errorâ€)
return myErr
}
err := myFunc()
if err != nil {
fmt.Println(err.Error())
}
This prints our defined error(My Error») to the screen.
The panic and recovery mechanisms work a bit differently. As panic halts program flow and causes the execution to exit with a non-zero status code, and then prints out the stack trace and error.
if err != nil {
panic(err)
}
When a function calls panic(), it won’t be executed further, but all deferred functions will be called. Recover is typically used with this defer mechanism to rescue the program from the panic.
func panicAndRecover() {
defer func() {
if err := recover(); err != nil {
fmt.Println(Successfully recovered from panicâ€)
}
}()
panic(Panicking")
}
When the panicAndRecover function is called, it panics, but rather than returning a non-zero exit status and exiting, it runs the deferred anonymous function first. This recovers from the panic using recover(), and prints out to the screen. Normal program execution occurs without exiting immediately as we successfully recover from panic.
API Error Handling
When building web APIs, appropriately handling errors is an integral part of the development process. Examples of such errors include JSON parsing errors, wrong endpoint requests, etc.
Let’s dive into some of the best practices for handling API errors in Go:
Using Appropriate HTTP Status Codes
HTTP status codes communicate the status of an HTTP request; you must carefully return status codes representing the state of a request.
HTTP status codes are split into five categories:
-
1xx — Information
-
2xx — Success
-
3xx — Redirection
-
4xx — Client error
-
5xx — Server error
Many developers using your API might rely solely on the status code to see if the request was successful. Sending a 200 (success) code followed by an error is bad practice. Instead, it is proper to return a more appropriate code, such as 400 (bad request).
For instance, consider the following reqChecker function, which takes a request body and returns nil or some error value, depending on the condition of the request body. An http.StatusOK
(200 status code) is misleading if the request body doesn’t conform to our standard.
func checker(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if err := reqChecker(r.Body); err != nil {
fmt.Fprintf(w, Invalid request body error: %sâ€, err.Error())
}
}
We can refactor the handler to return a more appropriate error code:
func checker(w http.ResponseWriter, r *http.Request) {
if err := reqChecker(r.Body); err != nil {
w.WriteHeader(http.StatusBadRequest) // 400 http status code
fmt.Fprintf(w, Invalid request body error:%sâ€, err.Error())
} else {
w.writeHeader(http.StatusOK)
}
}
There are quite a large number of HTTP status codes available, but just a subset of them are actually used in practice, with the most common ones being:
-
200 — Ok
-
201 — Created
-
304 — Not modified
-
400 — Bad Request
-
401 — Unauthorized
-
403 — Forbidden
-
404 — Not Found
-
500 — Internal Server Error
-
503 — Service unavailable
It’s not ideal to use too many status codes in your application. Keeping it to a minimum is recommended practice.
Descriptive Error Messages
While sending proper HTTP status error codes is a very important step in handling API errors, returning descriptive messages provides the client with additional information.
Multiple errors could return the same error code. For instance, posting a wrong JSON request format and malformed requests could spawn the same http.StatusBadRequest
error (status code 400). The status code indicates that the request failed due to the client’s error but didn’t provide much about the nature of the error.
Returning a JSON response to the client alongside the error code like the following is more descriptive:
{
"error": "Error parsing JSON request",
"message": "Invalid JSON format",
"detail": "Post the 'author' and 'id' fields in the request body."
}
Note: Descriptive error messages should be framed carefully not to expose the inner workings of the API to attackers.
The error field within the response should be unique across your application, and the detail field should provide more information about the error or how to fix it.
Avoid generic error messages, as it’s much better to know why specifically the request failed rather than a generic error code.
Exhaustive Documentation
Documentation is an essential part of API development. While API errors can be handled with ease by formulating proper responses when errors occur, comprehensive documentation helps the clients know all the relevant information concerning your API, such as what endpoints are provided by your API, the response and request formats, parameter options, and more.
The more exhaustive your documentation, the less likely clients will spend time battling API errors.
Conclusion
In this guide, we have learned about error handling in Go and some of the best practices for handling API errors.