Table of Contents

Why you (almost) never want to call os.Exit or log.Fatalln

Go’s defer is one of those things you quickly fall in love with. It’s perfect for cleaning up and neatly ensuring steps are always run when exiting some code block.

Many devs coming in from other languages might be used to calling some equivalent of os.Exit (e.g: sys.exit in python) or think the log package’s log.Fatalln is a handy way of “error and quit” but here’s the catch: if you call os.Exit or log.Fatalln, all those lovely deferred functions you’re relying on? Yeah, they don’t get called.

This might surprise many, especially given that panic does allow the defer stack to unwind.

What’s the issue?

Let’s dive straight into some code.

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    defer fmt.Println("This will NOT be printed")

    // Uncomment one of these to see the problem:
    // os.Exit(1)
    // log.Fatalln("Goodbye!")
}

While the example seems benign, in some cases you might have:

package main

import (
    "some.remote.repo.io/remoteLoggingPackage"
)

func main() {
    logger := remoteLoggingPackage.New()
    defer logger.Close()

    
    if err := run(); err != nil {
        logger.Errorf("error: ", err)
    }
}

If someone calls os.Exit or log.Fatalln somewhere inside run, or run returns an error, there’s a good chance the last few log calls will never get sent to the remote.

I’ve seen this in production where error logs start disappearing, and surprisingly the defer->debug.SetTraceback->debug.Stack block isn’t running only to see there’s no panic but instead someone snuck in an os.Exit call thinking it’d work similar to panic.

What’s the alternative?

Keep a tiny main, have it call another function (e.g: run), do all your defers in there and call os.Exit in main.

package main

import (
    "os"

    "some.remote.repo.io/remoteLoggingPackage"
)

func main() {
    os.Exit(run())
}

// no point returning errors, only error codes
func run() int {
    logger := remoteLoggingPackage.New()
    defer logger.Close()

    // set up your defers 

    // start code execution 

    // on errors / end of execution return appropriate exit code 
}

Always let errors bubble up, it’s the way to go with Go.