Golang edge case: when “defer” will never work out in your code

Wacław The Developer
2 min readSep 25, 2024

--

Today I want to show you some code, and maybe it will save you from creating tricky bugs in your project. Everything that I will show you was tried on Go 1.23.0. Let’s take a look at the code:

import (
"encoding/json"
"log"
)

func main() {
defer func() {
println("simulating cleanup process")
}()

//Getting the simple error as example
data := []byte("definitely not a JSON")
var result map[string]any
err := json.Unmarshal(data, &result)
if err != nil {
log.Fatal(err)
}
}

This code can be written to decode JSON and perform some cleanup after that. If you run that, you will figure out that “simulating cleanup process” will never appear on screen. Let’s take a look at what log.Fatal(err) does under the hood:

// Fatal is equivalent to [Print] followed by a call to [os.Exit](1).
func Fatal(v ...any) {
std.Output(2, fmt.Sprint(v...))
os.Exit(1)
}

The problem is that usage of os.Exit causes the instant termination without triggering the “defer” body.

The same result will be if instead of log.Fatal(err) you will call just os.Exit(1) or os.Exit(0).

What about panic() ?

Let’s replace log.Fatal call with panic:

import (
"encoding/json"
)

func main() {
defer func() {
println("simulating cleanup process")
}()

//Getting the simple error as example
data := []byte("definitely not a JSON")
var result map[string]any
err := json.Unmarshal(data, &result)
if err != nil {
panic(err)
}
}

This code will print “simulating cleanup process” as expected, so defer will be working.

What about panic() in goroutine?

Let’s modify the code and try to run decoding in a goroutine:

import (
"encoding/json"
"sync"
)

func main() {
defer func() {
println("simulating cleanup process")
}()

var wg sync.WaitGroup

wg.Add(1)
go func() {
defer wg.Done()
//Getting the simple error as example
data := []byte("definitely not a JSON")
var result map[string]any
err := json.Unmarshal(data, &result)
if err != nil {
panic(err)
}
}()
wg.Wait() //Waiting the goroutine ends execution

println("Done")
}

Surprise, but “simulating cleanup process” will not appear on your screen :)

Conclusion

The example in article is pretty simple, but in real life you can miss something important that should be done in “defer” construction. For example, you can miss flushing some buffer skipping some important crash logs.

--

--

No responses yet