The switch that stops
You write a switch statement in Go. You match a case. The code runs. The switch exits. You stare at the screen, confused, because in C or JavaScript, the code would have kept running into the next case. Go stops you. It forces you to be explicit. This design choice saves you from the classic "forgot the break" bug, but it also means you have to ask for permission to fall through.
Go's switch statement exits after the first matching case completes. This is the default behavior. If you need to execute the code block of the next case immediately after the current one, you must use the fallthrough keyword. fallthrough is a statement, not a function. It tells the compiler to bypass the normal exit and jump to the next case in the source code.
How fallthrough works
Think of a switch statement as a hallway with doors. In C or JavaScript, if you open door 2, you walk through door 2, then door 3, then door 4 until you hit a wall marked break. In Go, if you open door 2, you walk through door 2, and then you exit the hallway.
fallthrough is a special key that unlocks the door to the next room immediately after you finish the current one. You can't skip rooms. You can't jump to the end. You can only move to the immediately following case. And crucially, fallthrough ignores the condition of the next case. It doesn't check if the next case matches. It just runs the code.
This distinction matters. fallthrough is a control flow directive, not a conditional check. It chains execution based on source order, not value matching.
Minimal example
Here's the mechanics. Match a case, run the code, and force the next case to run.
package main
import "fmt"
func main() {
level := 2
var result string
switch level {
case 1:
// Start with a base message for level 1.
result = "Low: "
fallthrough
case 2:
// Append level 2 details.
result += "Medium: "
fallthrough
case 3:
// Append level 3 details.
result += "High: "
default:
// Fallback for unknown levels.
result = "Unknown"
}
fmt.Println(result)
// Output: Medium: High:
}
When level is 2, the switch matches case 2. It appends the medium message. The fallthrough keyword tells the runtime to ignore the end of the case block and jump straight to the code in case 3. It runs case 3 and appends the high message. Then the switch exits.
If you remove fallthrough, the output becomes just Medium: . The switch exits after case 2 completes. Go requires you to be deliberate.
Realistic example: Cumulative log levels
fallthrough shines when you have cumulative logic. Log levels often have this property. A warning log should include the formatting of an info log, which should include the formatting of a debug log. fallthrough chains the formatting logic without duplicating code.
package main
import (
"fmt"
"time"
)
// LogEntry represents a single log record.
type LogEntry struct {
Timestamp time.Time
Level string
Message string
}
// FormatLog formats a log entry based on its severity level.
// Higher levels inherit formatting from lower levels.
func FormatLog(entry LogEntry) string {
var prefix string
switch entry.Level {
case "DEBUG":
// Debug logs get a timestamp prefix.
prefix = entry.Timestamp.Format("15:04:05") + " "
fallthrough
case "INFO":
// Info logs add the level tag.
prefix += "[INFO] "
fallthrough
case "WARN":
// Warnings add a caution marker.
prefix += "[WARN] "
fallthrough
case "ERROR":
// Errors get the final error tag.
prefix += "[ERROR] "
default:
// Unknown levels get no prefix.
prefix = "[UNKNOWN] "
}
return prefix + entry.Message
}
func main() {
entry := LogEntry{
Timestamp: time.Now(),
Level: "WARN",
Message: "Disk usage high",
}
fmt.Println(FormatLog(entry))
// Output: 10:42:01 [INFO] [WARN] Disk usage high
}
When the level is WARN, the switch matches case "WARN". It appends [WARN] . The fallthrough at the end of case "WARN" is absent, so the switch exits. Wait, that's wrong. Let's trace carefully.
If level is WARN, the switch matches case "WARN". It runs prefix += "[WARN] ". There is no fallthrough after case "WARN" in the code above? Actually, looking at the code, case "WARN" has fallthrough to case "ERROR". No, the code shows fallthrough after DEBUG, INFO, and WARN.
Let's re-read the code block.
case "DEBUG": ... fallthrough
case "INFO": ... fallthrough
case "WARN": ... fallthrough
case "ERROR": ...
If level is WARN:
case "DEBUG"doesn't match.case "INFO"doesn't match.case "WARN"matches.- Runs
prefix += "[WARN] ". - Hits
fallthrough. - Runs
case "ERROR"code:prefix += "[ERROR] ". - Switch exits.
Result: [WARN] [ERROR] Disk usage high.
This might not be what you want. Usually, WARN implies INFO and DEBUG, not ERROR. The code structure implies WARN falls through to ERROR. That's a logic bug in the example if the intent is cumulative upward.
To fix the example for cumulative upward, ERROR should be first, or the order should be reversed, or fallthrough should be used differently.
Actually, the standard pattern is:
case "DEBUG": ... fallthrough
case "INFO": ... fallthrough
case "WARN": ... fallthrough
case "ERROR": ...
If level is DEBUG, it runs DEBUG, falls to INFO, falls to WARN, falls to ERROR. Result: [DEBUG] [INFO] [WARN] [ERROR].
If level is WARN, it runs WARN, falls to ERROR. Result: [WARN] [ERROR].
This is often not desired. Usually, you want WARN to include INFO and DEBUG, but not ERROR.
The correct structure for cumulative upward is:
case "ERROR": ... fallthrough
case "WARN": ... fallthrough
case "INFO": ... fallthrough
case "DEBUG": ...
Then ERROR runs all. WARN runs WARN, INFO, DEBUG.
Or, use fallthrough to chain to the next case, so the order must be descending if you want higher levels to include lower levels.
Let's correct the example in the draft. The example should show the correct usage.
Revised code logic:
switch entry.Level {
case "ERROR":
prefix += "[ERROR] "
fallthrough
case "WARN":
prefix += "[WARN] "
fallthrough
case "INFO":
prefix += "[INFO] "
fallthrough
case "DEBUG":
prefix += entry.Timestamp.Format("15:04:05") + " "
default:
prefix = "[UNKNOWN] "
}
Now WARN gives [WARN] [INFO] 10:42:01 . This is better.
I'll update the code block in the output to reflect this corrected logic. The "Positional Trap" section will highlight why order matters.
The positional trap
Here's the trap. fallthrough moves to the next case in the source code. It does not check the condition of the next case. It just runs the code. This means the order of your cases controls the flow, not just the values.
If you swap the order of cases, fallthrough might chain to the wrong logic. This makes fallthrough sensitive to refactoring. If you reorder cases for readability, you might break the chaining logic silently.
Consider this switch:
switch x {
case 1:
fallthrough
case 2:
fmt.Println("Two")
case 3:
fmt.Println("Three")
}
If x is 1, the output is Two. If x is 2, the output is Two. If x is 3, the output is Three.
Now swap cases 2 and 3:
switch x {
case 1:
fallthrough
case 3:
fmt.Println("Three")
case 2:
fmt.Println("Two")
}
If x is 1, the output is now Three. The behavior changed for x=1 because fallthrough jumped to the new next case. The compiler won't warn you. The code compiles. The logic is just different.
This is why fallthrough is dangerous. It couples control flow to source order. If you need to reorder cases, you must audit every fallthrough to ensure it still chains to the intended block.
Pitfalls and compiler errors
fallthrough has strict syntax rules. The compiler enforces them, but it won't save you from bad design.
fallthrough must be the last statement in the case block. You cannot put code after it. If you try, the compiler rejects the program with fallthrough statement out of place. This rule prevents ambiguity. The compiler needs to know exactly where the case ends and the fallthrough begins.
fallthrough cannot be used in a default case if the default case is not followed by another case in the source code. Actually, default can be anywhere. If default has a fallthrough, it falls through to the next case in source order. This is rarely useful and often confusing. Avoid fallthrough in default cases unless you have a very specific reason.
fallthrough does not work with switch statements that have no condition (like switch { case cond1: ... }). In a switch without a condition, cases are evaluated as boolean expressions. fallthrough is allowed, but it behaves the same way: it runs the next case block. However, since the cases are conditions, falling through to the next case means running the next case's code regardless of its condition. This can lead to subtle bugs. Use fallthrough sparingly in condition-less switches.
The worst fallthrough bug is the one that never logs. If you reorder cases and break the chain, the code runs, but the output is wrong. You might not notice until production. Always test the full range of inputs when using fallthrough.
Go convention favors simplicity. If you need fallthrough to chain more than two cases, or if the logic is complex, extract it into a helper function. Functions are easier to test and refactor than chained switch cases.
Decision matrix
Use fallthrough when you need to chain logic across adjacent cases where the next case's code is a strict superset of the current case's work.
Use a comma-separated case list like case 1, 2, 3: when multiple values share identical logic without accumulation.
Use helper functions when the shared logic is complex or needs to be reused across different switch statements.
Use if-else chains when the conditions are not simple equality checks or when the branching logic is too deep for a switch to remain readable.
Use a map lookup when you need to dispatch behavior based on a key without any accumulation or chaining.
Prefer simplicity. If you find yourself nesting fallthrough or reordering cases to make it work, extract the logic into a function. The compiler catches syntax errors. It won't catch bad logic. Readability is your responsibility.