Missing Return at End of Function Error in Go Explained
Go rejects a function with results unless every path ends in a terminating statement. See which statements count, why log.Fatal does not and the fixes.

You wrote a function that returns on every path you can think of, and the compiler says missing return at the closing brace. It is almost always right. The rule it applies is mechanical and slightly stricter than human reasoning, which is why a function that looks sure to return can still fail it. This article lists the statements the language counts as terminating, runs the shapes that fail, and shows the three fixes with the one I prefer. It follows the switch article, where the same error appears, and functions with multiple returns. It is part of the Go basics path.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
Three functions that fail
A function with a result must end in what the specification calls a terminating statement. Here are the three shapes I meet most, each failing for a different reason:
package main
import "fmt"
func sign(n int) int {
if n > 0 {
return 1
} else if n < 0 {
return -1
}
}
func firstEven(xs []int) int {
for {
for _, x := range xs {
if x%2 == 0 {
break
}
}
break
}
}
func label(n int) string {
switch n {
case 1:
return "one"
default:
fmt.Println("other")
}
}
func main() {
fmt.Println(sign(1), firstEven(nil), label(1))
}# example.test/lab
./main.go:11:1: missing return
./main.go:22:1: missing return
./main.go:31:1: missing returnEach error is missing return, reported at the closing brace of the function. The messages come from different causes.
sign has an if and an else if and no final else. Both conditions can be false, n == 0, and then control falls off the end. A human knows n is an int and the three cases cover everything, and the compiler does not reason about values. It looks at the syntax: an if statement terminates only if it has an else and both branches terminate.
firstEven has a for with no condition, which would be terminating, since it loops forever unless broken. But the loop contains a break that targets it, so it can exit normally, and then execution reaches the end. The inner break belongs to the inner range loop. The outer break is the one that counts.
label has a switch with a default, and the default case prints and does not return. A switch is terminating only when it has a default and every case ends in a terminating statement, with no break pointing at the switch. One non-returning branch is enough to fail the whole statement.
What counts as terminating
The specification's list of terminating statements is short, and you can learn it as a table:
| Statement | Terminating when |
|---|---|
return | Always |
goto | Always |
Call to the builtin panic | Always |
A block { ... } | Its last statement is terminating |
if ... else ... | Both branches are terminating |
for with no condition and no range | No break refers to it |
switch | There is a default, no break refers to it, and every case list ends in a terminating statement or fallthrough |
select | No break refers to it and every case ends in a terminating statement |
| A labeled statement | The labeled statement is terminating |
And here are functions that satisfy the rule, even though they look like they might not:
package main
import "fmt"
func forever() int {
for {
}
}
func ifElse(n int) int {
if n > 0 {
return 1
} else {
return 2
}
}
func crash() int {
panic("never returns")
}
func sw(n int) string {
switch {
case n > 0:
return "positive"
case n < 0:
return "negative"
default:
return "zero"
}
}
func main() {
fmt.Println(ifElse(1), sw(0))
_ = forever
_ = crash
}1 zeroforever has no return statement at all and compiles, because an unconditional for with no break never reaches the end. crash compiles because panic is terminating. sw shows the idiomatic version of the switch: a default that returns. The program prints 1 zero, and the other two functions are never called, only referenced so the compiler does not complain about them being unused.
The functions that end the process
The case that confuses people most is a function that ends with a call that never returns:
package main
import (
"fmt"
"log"
"os"
)
func mustOpen(path string) *os.File {
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
return f
}
func fail(msg string) int {
fmt.Println(msg)
os.Exit(1)
}
func main() {
_ = mustOpen
_ = fail
}# example.test/lab
./main.go:20:1: missing returnmustOpen is fine, because it has a final return f, and I included it for contrast. fail ends with os.Exit(1), which stops the process, and the compiler reports missing return anyway. Only the builtin panic counts as terminating. os.Exit, log.Fatal, t.Fatal and runtime.Goexit are ordinary functions as far as the type checker is concerned, even though they never come back. That is a design choice: making a package function special would mean the language rule depends on a library. The fix is to add a return that will never run, or to panic:
func fail(msg string) int {
fmt.Println(msg)
os.Exit(1)
return 0 // unreachable, satisfies the compiler
}The unreachable return is the accepted idiom for this case, and go vet does not complain about it, because it cannot tell either. If the function really is about failing, return an error and let main decide to exit. A library function that calls os.Exit takes the choice away from every caller and cannot be tested.
Three fixes, in order of preference
Return on every branch. Restructure so the last statement is a plain return, usually by making the failure case the default:
func sign(n int) int {
if n > 0 {
return 1
}
if n < 0 {
return -1
}
return 0
}Early returns with a final return of the "fall through" value read well and are the dominant Go style. They also avoid else after a return, which linters flag.
Add a default to the switch that either returns a value or panics when reaching it means a bug. For an enum, default: panic(fmt.Sprintf("unknown level %d", l)) makes the failure loud instead of returning a wrong value. For input from outside, return an error, or a clear placeholder.
Loops that search. A function that loops looking for something should return the result inside the loop when it finds it, and return the "not found" value after the loop. The firstEven case becomes:
func firstEven(xs []int) (int, bool) {
for _, x := range xs {
if x%2 == 0 {
return x, true
}
}
return 0, false
}The second result tells the caller whether the search succeeded, which is better than a sentinel such as -1, as functions that return an error or a bool explains.
Related messages
If a function with no result ends with a return that has a value, you get too many return values. If a function with a result has a bare return and the results are not named, you get not enough return values. And where a function is expected to return a result, a return with the wrong type gives the familiar cannot use value as type. Dead code after a terminating statement does not cause an error, but go vet reports it as unreachable code.
When the rule feels wrong
The rule is syntactic on purpose. Value analysis would make the compile result depend on the checker's cleverness, and a different compiler could accept different programs. The cost is the occasional artificial return. In generated code and in functions whose last branch is a long state machine, a final panic("unreachable") documents the assumption and satisfies the rule. If you find yourself adding it often, it may be a sign that a type, such as an enum, should be handled by a switch with a default that fails loudly. The authoritative text is the language specification on terminating statements, and the Go FAQ discusses the design trade-off behind the compiler's strictness.


