1. Background

Recently, during a joint debugging session, I encountered a situation where a gRPC Server returned nil for a specific interface. Since the Server was functioning normally and the interface was implemented, there were no errors during the call. However, the program suddenly terminated without any log output. The issue was easily reproducible by simulating the return of nil data. In Debug mode, I could see the panic and the call stack information. It turned out that the panic was caused by accessing an internal field of a nil value. Although the root cause was incorrect handling on the Server side, it was also necessary for the Client to implement safeguards to catch the panic.

2. Concepts

1. Panic

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

Common causes of panic include nil pointer dereference, array out-of-bounds access, and concurrent map writes.

2. Recover

Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

Recover is another built-in function in Go that is used to catch panic-induced errors, preventing the program from crashing outright.

3. Implementation

The initial code structure was roughly as follows:

 1func main() {
 2	go func() {
 3        for {
 4			select {
 5			case <-c.menuItems.start.ClickedCh:
 6				getSthViaGRPC()
 7		}
 8	}()
 9}
10
11func getSthViaGRPC() string {
12	panic("")
13}

At first, I naively thought that adding recover in the main goroutine would solve the problem once and for all. However, the program still crashed. Moving recover to the getSthViaGRPC function worked, but it was impractical to implement this in every function. After some research, I found that panic only triggers defer in the current goroutine. Since I had started a new goroutine, I needed to catch the panic within that goroutine.

 1go func() {
 2    defer func() {
 3        if r := recover(); r != nil {
 4			logger.Errorf("Error: %v\nStack trace:\n %s", r, string(debug.Stack()))
 5		}
 6    }()
 7    for {
 8        select {
 9        case <-c.menuItems.start.ClickedCh:
10            getSthViaGRPC()
11    }
12}()

The good news was that the panic was finally caught. The bad news was that the program froze. Upon reviewing the code, I realized that although the panic was caught, the for loop had exited, rendering the program useless. I then encapsulated the logic into a safeHandler:

 1func (c *AppCommand) safeHandler(handler func()) {
 2	defer func() {
 3		if r := recover(); r != nil {
 4			logger.Errorf("Error: %v\nStack trace:\n %s", r, string(debug.Stack()))
 5		}
 6	}()
 7	handler()
 8}
 9
10go func() {
11    defer func() {
12        if r := recover(); r != nil {
13			logger.Errorf("Error: %v\nStack trace:\n %s", r, string(debug.Stack()))
14		}
15    }()
16    for {
17        select {
18        case <-c.menuItems.start.ClickedCh:
19            safeHandler(getSthViaGRPC)
20    }
21}()

4. Summary

  1. Panic only triggers defer in the current goroutine. It does not work across goroutines, but it allows for nested calls within defer.
  2. Recover only works when called within a defer function.

5. Common Panics

  1. panic: assignment to entry in nil map

    Attempting to assign a value to an uninitialized map. In Go, map is a reference type and must be initialized using make or a literal before use. Otherwise, its value is nil, and trying to insert data into a nil map will cause a runtime panic.