Error Handling in Goroutines: When Life Gives You Exceptions, Make Exceptional Code

Learn how to handle error in multiple goroutines with just using wait group.

Inshal Khan
8 min readFeb 18, 2023
Go with the flow, let goroutines handle the rest!

Goroutines

Goroutines are a feature of the Go programming language that allow you to achieve concurrent execution of code. Concurrency is the ability of a program to execute multiple tasks at the same time, without having to wait for one task to finish before starting the next.

A real-life example of concurrency can be seen in a restaurant kitchen. The chef can prepare multiple dishes at the same time by delegating tasks to different cooks. For instance, one cook could be preparing the appetizers, while another is working on the main course, and yet another is baking desserts. By dividing the work among different cooks, the chef can get all the dishes prepared and ready to serve more quickly.

In programming, goroutines work in a similar way. You can create multiple goroutines, each executing a different task, and they can run concurrently. This can be useful for programs that need to perform multiple tasks simultaneously, such as downloading data from multiple sources, handling multiple client connections, or processing data in parallel.

Here’s a simple example of how to use goroutines in Go to calculate the factorial of a number:

package main
import (
"fmt"
)
func factorial(n int) int {
if n == 0 {
return 1
}
return n * factorial(n - 1)
}
func main() {
n: = 10
// calculate the factorial of n using a goroutine
go func() {
result: = factorial(n)
fmt.Printf("Factorial of %d is %d\n", n, result)
}()
// wait for the goroutine to finish
var input string
fmt.Scanln( & input)
}
Factorial of 10 is 3628800
done

In this example, we define a factorial function that calculates the factorial of a number using recursion. We then create a goroutine to calculate the factorial of n, which is set to 10. The go keyword before the function call tells Go to execute the function in a new goroutine.

Finally, we wait for the goroutine to finish by reading a line of input from the user. This allows us to keep the program running until the goroutine has completed its task. Once the user presses Enter, the program will exit.

This is a simple example, but it demonstrates how you can use goroutines to perform tasks concurrently in Go.

Channels

Channels in Go

Imagine you are running a restaurant and you have a chef and a waiter. The chef is responsible for cooking the food and the waiter is responsible for delivering the food to the customers. The chef and the waiter need to communicate with each other to coordinate their work.

In this scenario, the channel can be used as a means of communication between the chef and the waiter. The chef can put the cooked food on a plate and send it down a channel to the waiter, who is waiting on the other end to receive it. The waiter can then take the plate of food and deliver it to the customer.

Here’s a code example that demonstrates this scenario:

package main

import (
"fmt"
"time"
)

func main() {
// Create a channel to communicate between the chef and the waiter
orders := make(chan string)

// Chef prepares the food and sends it down the channel
go func() {
for i := 1; i <= 5; i++ {
fmt.Println("Chef is preparing order", i)
time.Sleep(2 * time.Second)
orders <- fmt.Sprintf("Order %d", i)
fmt.Println("Chef sent order", i, "down the channel")
}
close(orders)
}()

// Waiter receives the food from the channel and delivers it to the customer
for order := range orders {
fmt.Println("Waiter received order from the channel:", order)
time.Sleep(1 * time.Second)
fmt.Println("Waiter delivered order to the customer:", order)
}

fmt.Println("All orders have been delivered!")
}
Chef is preparing order 1
Chef sent order 1 down the channel
Chef is preparing order 2
Waiter received order from the channel: Order 1
Waiter delivered order to the customer: Order 1
Chef sent order 2 down the channel
Chef is preparing order 3
Waiter received order from the channel: Order 2
Waiter delivered order to the customer: Order 2
Chef sent order 3 down the channel
Chef is preparing order 4
Waiter received order from the channel: Order 3
Waiter delivered order to the customer: Order 3
Chef sent order 4 down the channel
Chef is preparing order 5
Waiter received order from the channel: Order 4
Waiter delivered order to the customer: Order 4
Chef sent order 5 down the channel
Waiter received order from the channel: Order 5
Waiter delivered order to the customer: Order 5
All orders have been delivered!

In this example, the chef is a goroutine that prepares orders and sends them down the orders channel. The waiter is the main routine that receives orders from the orders channel and delivers them to the customer. The communication between the two is coordinated using the channel. The chef sends orders down the channel and the waiter receives orders from the channel, one at a time. When all orders have been delivered, the orders channel is closed, and the program terminates.

Error handling in Goroutines

When dealing with error handling in multiple goroutines, it’s important to ensure that errors are properly handled and communicated back to the main thread. There are a few key strategies to keep in mind when handling errors in this context:

  1. Use channels for communication: Channels are the preferred way to communicate between goroutines in Go, and they can also be used to pass errors between goroutines. You can create a dedicated error channel that each goroutine can use to send errors back to the main thread.
  2. Use a wait group: A wait group is a synchronization primitive that can be used to block the main thread until all the goroutines have completed. This can be useful for ensuring that all errors are properly handled before the program exits.
  3. Use a deferred function to handle errors: When working with goroutines, it’s important to handle errors as soon as possible, as they can quickly accumulate and cause issues. One way to handle errors quickly is to use a deferred function that is called when the goroutine completes. This function can check for errors and send them back to the main thread via a channel.
  4. Use context to manage timeouts and cancellations: When working with multiple goroutines, it can be difficult to manage timeouts and cancellations. Go’s context package provides a way to manage these scenarios, allowing you to set a deadline for a group of goroutines and cancel them if necessary.

By following these strategies, you can ensure that errors are properly handled in a concurrent context, reducing the risk of bugs and making your code more reliable.

Here’s an example of how error handling can be done in multiple goroutines:

func main() {
c := make(chan error)

for i := 0; i < 10; i++ {
go func() {
// do some work
err := doSomething()
c <- err
}()
}

for i := 0; i < 10; i++ {
if err := <-c; err != nil {
log.Println(err)
}
}
}

func doSomething() error {
// do some work
if err != nil {
return fmt.Errorf("error occurred: %v", err)
}
return nil
}

To handle errors in multiple goroutines and cancel their execution if an error occurs in any of them, we can use a combination of channels and a synchronization mechanism such as a wait group.

Here’s an example code that demonstrates this approach:

package main

import (
"context"
"errors"
"log"
"sync"
)

func main() {
// Create a context with a cancel function
ctx, cancel := context.WithCancel(context.Background())

// Create a channel to receive fatal errors
fatalErrorChannel := make(chan error)

// Create a channel to signal main routine for fone
wgDone := make(chan bool)

// Use a waitgroup to wait for all goroutines to finish
var wg sync.WaitGroup

// Spawn some goroutines
for i := 0; i < 5; i++ {
wg.Add(1)
go func(num int) {
// Defer the waitgroup's Done function
defer wg.Done()

// Check if the context has been cancelled before proceeding
select {
case <-ctx.Done():
log.Println("Context cancelled, exiting goroutine", num)
return
default:
// Do some work
log.Println("Starting work in goroutine", num)
err := doWork()
if err != nil {
// Send the error to the fatal error channel and cancel the context
log.Println("Error encountered in goroutine", num, ", cancelling context")
fatalErrorChannel <- err
cancel()
}
log.Println("Finished work in goroutine", num)
}
}(i)
}



go func() {
// Wait for all goroutines to finish
wg.Wait()
close(wgDone)
}()

// Check for any fatal errors
select {
case <-wgDone:
break
case err := <-fatalErrorChannel:
close(fatalErrorChannel)
log.Fatal("Error encountered: ", err)
}
log.Println("Program worked!")
}

func doWork() error {
// Do some work
return errors.New("Something went wrong")
}
Starting work in goroutine 0
Starting work in goroutine 1
Starting work in goroutine 4
Error encountered in goroutine 4 , cancelling context
Finished work in goroutine 4
Error encountered: Something went wrong
exit status 1

The above code demonstrates how to handle errors in multiple goroutines and cancel their execution if an error occurs in any of them using channels, wait groups, and context.

The main function starts by creating a context with a cancel function and channels to receive fatal errors and signal the main routine. A wait group is also used to wait for all the goroutines to finish their work.

Next, the code spawns some goroutines using a loop and the go keyword. Each goroutine executes the doWork() function and sends the error, if any, to the fatal error channel. Before doing any work, each goroutine checks if the context has been cancelled. If the context is cancelled, the goroutine exits without performing any work.

After spawning the goroutines, the code creates another goroutine that waits for all the other goroutines to finish and closes the wgDone channel.

Finally, the code waits for any fatal error on the fatalErrorChannel. If a fatal error is encountered, the context is cancelled, and the error message is logged using log.Fatal(). If no fatal errors are encountered, the main function logs "Program worked!" to indicate successful execution.

The doWork() function is just a dummy function that always returns an error to demonstrate the error handling mechanism. In a real-world scenario, this function would contain the actual work that needs to be done by the goroutines.

Note that this is just one example of how error handling can be done in multiple goroutines, and the exact implementation will depend on the specific needs of your program.

--

--

Inshal Khan
Inshal Khan

Written by Inshal Khan

Operating Systems| IoT | API developer.

No responses yet