The Good, Bad, and Ugly of Go Errors

Sonu Kumar
6 min readJul 26, 2020

--

Compared to Java/C++, the Go programming language offers numerous advantages such as Channels, For loops that replace other loops like do and while, and Go routines. However, Go also has some inconsistencies, such as when using a range on a channel in a for loop, only the channel item is returned, whereas iterating over an array/slice provides the index as well. Additionally, Go’s error handling has not progressed much over time, which can cause frustration for developers. Various things can happen with errors in Go, including panics, which can terminate a program as outlined in the Go documentation. When a panic occurs in a Go routine and is not handled, the entire application may crash, making panics particularly intriguing.

The panic built-in function stops normal execution of the current goroutine. When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic, terminating G's execution and running any deferred functions. This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated with a non-zero exit code. This termination sequence is called panicking and can be controlled by the built-in function recover.

Error Handling in Go

In contrast to programming languages such as C++, Java, Scala, Ruby, and Python, Go does not have a concept of exception propagation. Instead, each method in Go typically returns an error interface in the return values, along with any other expected values. As a result, a basic Go function may return an error, indicating that an unexpected condition has occurred.

The caller method handles the error by looking at the error in function call return values, something like this

In addition to the built-in error interface, Go also allows developers to define their own custom errors, as long as they implement the Error interface. Different libraries or packages may return varying types of errors, but they all conform to the Error interface. This approach to error handling is effective, except when a panic occurs, which can abruptly halt program execution as previously explained. Proper panic handling is crucial in Go programs, particularly when multiple goroutines are involved, as a failure in one goroutine can cause the entire application to crash. Panic is handled using the defer and recover methods.

This type of panic handling creates two major problems

  • Error Reporting: When a panic occurs in a Go routine, it does not report the cause of the failure to the main goroutine, making it difficult to understand why it failed. As a result, if the main goroutine needs to reset some state in response to a child goroutine’s failure, it cannot do so since it is unaware that the child goroutine has been terminated. This highlights the importance of properly handling panics in Go programs, especially when working with multiple goroutines. By implementing defer and recover methods, developers can catch and handle panics in their programs, allowing for better error reporting and recovery.
  • Code Duplication: In Go, panics are typically handled using a standard approach that involves the use of defer and recover methods. When a panic occurs, any deferred functions are executed before the panic propagates up the call stack. The recover function can then be called to capture the panic value and allow the program to continue execution. In a typical error handling scenario, a deferred function is used to call the recover function and log the error, along with any additional operations that may be required, such as taking a memory snapshot or reporting the error to an error reporting system like NewRelic or DataDog. This approach ensures that panics are properly handled and that the program can recover from errors and continue executing, without crashing the entire application.

For example, In Java, we can do something like this

No matter what types of exceptions occurred, exceptions will be caught, on successful exception catching we can either take some actions or terminate the program, even this program can handle the case where the method call would have created multiple threads.

Error Propagation in Go

Go does not have a traditional concept of threads like some other programming languages do. Instead, Go supports goroutines, which are lightweight threads that are managed by the Go runtime.

In Go, any number of goroutines can be launched or created using the go keyword, followed by a function call. For example, calling go foo(...) will create a goroutine to execute the function foo() as per the Go runtime's scheduling policy.

One important thing to note is that goroutines cannot return errors to the function that launched them, but they can communicate with other goroutines using channels or other mechanisms. This allows for efficient communication and synchronization between goroutines and is one of the key features that make Go an excellent language for concurrent programming.

We can model our goroutine to send the panic error back to the parent or any other goroutine using a channel. Let’s define a routine interface that supports a few things,

  • Id (it’s pseudo id, as go does not expose goroutine id like C++ thread id).
  • Run (this method will create a goroutine and run the given method)
  • Callback any callback method that would be called post the execution of the actual method
  • Defer any defer method that should be called on the completion of the actual method and callback.
  • ErrorReporting whether panic reporting is enabled or not
  • Msg any message that would be sent on the channel
  • ExecutionTime reports the total execution time of this goroutine

The message that would be sent over the channel

Now, we can implement the routine interface as

The NewRoutine method creates a new routineImpl object, which can be used to update other fields related to the goroutine. It takes two parameters: name and methodToRun.

The name parameter is used to specify the name of the goroutine, while methodToRun is a function that will be called from the goroutine to execute its logic. Note that methodToRun does not return anything. However, if it encounters an error, the error can be passed on to the error channel as a return value.

Overall, NewRoutine is a useful method for creating and managing goroutines in a Go program. By specifying a name and a method to run, developers can easily create and manage concurrent processes in a structured and controlled manner.

The current implementation of the Run method runs the methodToRun function in a goroutine. However, it only reports a panic error and does not handle any other errors that may occur in the methodToRun function.

Now we have a full implementation of the Routine interface, let’s see how to use this in a Go program.

For example, we can create a simple Printer interface that prints, some ids and terminates, it does not lead to any panic but it suffices the example, as we need to just call any method that can lead to panic error.

Also, an ErrorHandler is created that handles the error sent over the error channel.

In this simple example, 10 Routines have been created all of them are running in parallel. Error is reported to the main method using errorChannel and handled by ErrorHandler’s handle method.

To handle other errors, you could modify the methodToRun function to return an error value. Then, in the Run method, you could check for errors returned by methodToRun and send them on the error channel if they occur. This way, you can properly handle any errors that occur during the execution of the goroutine.

Here’s an example of how you could modify the Run method to handle errors returned by methodToRun:

If you found this post helpful, please share and give a thumbs up.

--

--