skip to content

In Go, what exit status does a process report when main returns, and how do you exit non-zero?

level: juniorimportance: should knowfreq 50%

answer

  1. silence means success
  2. there is no return 1 in main
  3. the os package ends the process
  4. one call, one integer, no defers

basics

~20 s

When main returns, the Go runtime ends the process with status 0, which every caller reads as success. To report failure you must call os.Exit with a non-zero code; os.Exit(1) is the conventional failure status.

solid answer

~40 s

Returning from `func main` always ends the process with status 0 - there is no `return 1` in Go, and returning after printing an error still tells the shell, a CI job or a supervisor that the run succeeded. To fail, you call `os.Exit(1)` (or another non-zero code) explicitly, usually after writing an explanatory message to `os.Stderr`. `log.Fatal` is shorthand for exactly that: print, then `os.Exit(1)`. Two caveats matter. `os.Exit` terminates immediately and runs no deferred calls, so it belongs at the top of the program rather than deep inside it. And on Unix only the low 8 bits of the code reach the parent, so codes are kept small - 0 for success, 1 for the ordinary failure, and anything else only if you document what it means.

code

go · 7 lines
go
func main() {
	if err := checkSchema(); err != nil {
		fmt.Fprintln(os.Stderr, "check failed:", err)
		os.Exit(1)
	}
	// reaching the end of main exits the process with status 0
}

go deeper

for a junior

Be ready to say that reaching the end of main exits with 0 and that os.Exit(1) is how you signal failure. Knowing that log.Fatal is a print followed by os.Exit(1) is enough at this level.

for a middle

Explain that the runtime sets 0 on return and that os.Exit is the only way to choose another number, that os.Exit runs no deferred calls, and that an unrecovered panic exits with 2.

for a senior

Show that you treat the status as the program's contract with whatever supervises it: a CI step, a shell chain or a restart policy reads only that integer, so a failure has to reach it and not just the log.

for a principal

Own the convention across your tools: which codes exist, whether anything beyond 0 and 1 carries meaning, and whether callers are allowed to branch on numbers you might later want to change.

## What an exit status is Every process ends with a small integer that its parent can read: a shell, a CI runner, a container runtime, a supervisor. Zero means success; any other value means failure. Nothing else about the run travels back that way - not the log lines, not the error message, only the number. This is why a tool can be perfectly informative on screen and still be reported as a success. ## Returning from main Go fixes the signature of the entry point as `func main()`. It takes no arguments and returns nothing, so there is no `return 1`. When `main` returns, the runtime shuts the process down with status **0**. That is true regardless of what happened inside. If you caught an error, printed it, and returned, the process still reports success. It is also true if other goroutines are still running: when `main` returns the process ends and they are not waited for. ## Setting a non-zero status The only way to choose the status yourself is `os.Exit(code int)`: - it does not return; - it ends the process immediately; - **it runs no deferred calls anywhere** - not in the calling function, not in `main`, not in any other goroutine. Because of that last property, the idiom is to keep `os.Exit` in `main` and let everything beneath it return errors, so that all the deferred cleanup has already run by the time the process ends. `log.Fatal`, `log.Fatalf` and `log.Fatalln` are convenience wrappers: they print through the standard logger and then call `os.Exit(1)`. They inherit the no-defers behaviour exactly. The neighbouring `log.Panic` family is different - it prints and then panics, which unwinds and runs deferred calls. ## The statuses a Go program produces - **0** - `main` returned, or `os.Exit(0)` was called. - **whatever you passed** - `os.Exit(n)`. - **2** - a panic that nothing recovered. The runtime writes the panic value and a dump of the goroutines to standard error and exits with 2, so a crash is distinguishable from a deliberate `os.Exit(1)`. ## Choosing the numbers On Unix the status is delivered to the parent through only 8 bits, so `os.Exit(256)` arrives as 0 and would be read as success. Keep codes in the 0-125 range: shells reserve 126 for "found but not executable", 127 for "not found", and 128+n for "killed by signal n". Most Go tools use only 0 and 1. If you use more, remember that the numbers become a contract - a pipeline that branches on "exit 3 means nothing to migrate" will break the day you renumber. Document the codes next to the tool, and treat adding one as an interface change. ## The practical habit Write the failure message to `os.Stderr`, not `os.Stdout`, so that a caller piping the tool's real output somewhere still sees the complaint. Then set the status. Message and status are two separate obligations, and only the second one is machine-readable.

  • Does printing an error to os.Stderr and then returning from main make the run count as a failure?
    No. Callers judge the status, and returning from main sets it to 0 no matter what was printed. A CI step or a shell chain joined with && will treat the run as successful. You have to call os.Exit with a non-zero code, or use log.Fatal, which does it for you.
  • What exit status does a Go program end with if a panic is never recovered?
    2. The runtime prints the panic value and a dump of the goroutines to standard error and then exits with status 2, so a crash is distinguishable from a deliberate os.Exit(1). To a caller both simply mean failure; the specific number only helps if your program documents it.
  • Does os.Exit(256) report 256 to the shell?
    On Unix, no. Only the low 8 bits survive, so 256 arrives as 0 and the run looks successful. Keep exit codes in the 0-125 range, since shells already use 126 for a file that cannot be executed, 127 for one that was not found, and 128+n for a process killed by signal n.

The status is the single checkbox on the form the caller reads. Everything you print is a cover letter nobody automated ever opens.

saying these in an interview costs you the question

  • Thinks func main can return an int to set the status
  • Believes printing to os.Stderr makes the run fail
  • Assumes os.Exit runs deferred cleanup before terminating
  • Expects os.Exit(256) to reach the shell as 256