Exit
Prefer returning an error from main over os.Exit so deferred cleanup runs -- use non-zero exit codes consistently because CI/CD pipelines and shell scripts rely on them.
os.Exit terminates the process immediately with the given exit code. Deferred functions are not run. Exit code 0 means success; any non-zero value signals failure. Shell scripts, CI/CD pipelines, and orchestrators all inspect the exit code.
os.Exit exits immediately -- no deferred functions, no finalizers. Use it at program boundaries where there is nothing to clean up, not in the middle of a call stack.
package main
import (
"fmt"
"os"
)
func main() {
args := os.Args[1:]
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "usage: program <name>")
os.Exit(1)
}
fmt.Println("hello,", args[0])
}The idiomatic Go pattern is a run() function that returns an error. main calls run, prints any error to stderr, and exits with code 1. Deferred functions inside run execute normally because os.Exit is only called from main.
package main
import (
"errors"
"fmt"
"os"
)
func run() error {
args := os.Args[1:]
if len(args) == 0 {
return errors.New("usage: program <name>")
}
fmt.Println("hello,", args[0])
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}log.Fatal logs the message then calls os.Exit(1). It is a shorthand for startup failures where structured error handling is not yet set up.
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
// log.Fatal calls os.Exit(1) after logging -- deferred functions do not run.
log.Fatal(http.ListenAndServe(":8080", nil))
}In production
Prefer the run() error pattern over calling os.Exit directly from inside your business logic -- this keeps deferred cleanup (closing files, flushing buffers, draining queues) working correctly because os.Exit skips all deferred functions when called anywhere in the call stack. log.Fatal is acceptable for startup failures (log.Fatal("failed to connect to db:", err)) where there is nothing to clean up yet, but it is wrong everywhere else in a running server -- use structured error returns and let the top-level handler decide whether to exit. Use non-zero exit codes consistently and document them: 0 = success, 1 = general error, 2 = misuse of shell builtins (by convention). CI/CD systems, make, and shell scripts all check exit codes; a process that always exits 0 even on failure silently swallows errors in pipelines. To inspect the exit code of a subprocess, use errors.As(err, &exitErr) on the error from cmd.Wait() and call exitErr.ExitCode().
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free