Skip to main content
The os.Exit function immediately terminates your program with a specific exit status code. This is important for shell scripts and other programs that check the success or failure of commands.

Basic Usage

package main

import (
    "fmt"
    "os"
)

func main() {
    // This defer will NOT run
    defer fmt.Println("!")

    // Exit with status 3
    os.Exit(3)
}
Defers do not run with os.ExitWhen you call os.Exit, the program terminates immediately. Deferred functions are NOT executed. This is a critical difference from normal function returns.

Exit Status Codes

Exit codes communicate program results to the shell:

0 - Success

The program completed successfully without errors

1 - General Error

A general error occurred (most common error code)

2 - Misuse

Typically indicates invalid command-line arguments

Custom Codes

Use codes 3-255 for application-specific errors

Checking Exit Status

Using go run

When you use go run, Go intercepts the exit status:
$ go run exit.go
exit status 3

Using a compiled binary

With a compiled binary, you can check the exit code directly:
$ go build exit.go
$ ./exit
$ echo $?
3
The $? shell variable contains the exit status of the last command.

Exit vs Return from main

func main() {
    if err := doSomething(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)  // Explicit exit code
    }
}
In Go, returning from main always results in exit code 0. Unlike C, Go does not support returning an integer from main to indicate exit status. If you need a non-zero exit code, you must use os.Exit.

Common Patterns

Error Exit Helper

Create a helper function for error exits:
func exitOnError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

func main() {
    file, err := os.Open("data.txt")
    exitOnError(err)
    defer file.Close()
    
    // Continue with file operations...
}

Multiple Exit Codes

Use different codes for different error types:
const (
    ExitSuccess = 0
    ExitError   = 1
    ExitUsage   = 2
    ExitNoInput = 3
    ExitNoHost  = 4
)

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintln(os.Stderr, "Usage: program <filename>")
        os.Exit(ExitUsage)
    }
    
    file, err := os.Open(os.Args[1])
    if err != nil {
        fmt.Fprintf(os.Stderr, "Cannot open file: %v\n", err)
        os.Exit(ExitNoInput)
    }
    defer file.Close()
    
    // Process file...
    os.Exit(ExitSuccess)
}

Cleanup Before Exit

Since defers don’t run, you need explicit cleanup:
func main() {
    exitCode := run()  // Do actual work in a function
    os.Exit(exitCode)
}

func run() int {
    file, err := os.Open("data.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        return 1
    }
    defer file.Close()  // This WILL run
    
    // Do work...
    
    return 0  // Success
}
This pattern allows defers to run properly because you’re returning from run, not calling os.Exit directly.

Example Output

$ go run exit.go
exit status 3
Notice that the ! from the deferred fmt.Println never appears.
$ go build exit.go
$ ./exit
$ echo $?
3

When to Use os.Exit

  • You need to signal specific error conditions to shell scripts
  • Building CLI tools that follow Unix conventions
  • You need immediate termination without cleanup
  • Implementing fail-fast behavior for critical errors
  • You need to run deferred cleanup functions
  • Writing library code (let callers decide how to handle errors)
  • In the middle of complex operations that need unwinding
  • Writing tests (use testing.T methods instead)
panic is different from os.Exit:
  • panic unwinds the stack and runs defers
  • panic can be recovered with recover()
  • os.Exit terminates immediately without unwinding
  • os.Exit cannot be caught
Use panic for programming errors, os.Exit for controlled termination.

Standard Exit Codes

While you can use any code from 0-255, following conventions helps:
CodeMeaningUsage
0SuccessProgram completed successfully
1General errorDefault error condition
2MisuseInvalid command-line arguments
126Command can’t executePermission problem or not executable
127Command not foundPath issue or typo
128+nFatal error signal “n”Program died due to signal n
130Script terminated by Ctrl+CSIGINT (signal 2)
255Exit status out of rangeUsed when exit code exceeds valid range
These conventions come from the Bash documentation and are widely followed in Unix-like systems.

Signals

Handle termination signals gracefully

Logging

Log errors before exiting

Build docs developers (and LLMs) love