Index Out of Range Panics in Go and How to Avoid Them
Fix panic index out of range in Go: read the message, find off-by-one loops, empty slices, os.Args and Split results, and guard accesses safely.

The message is one of the clearest in Go: panic: runtime error: index out of range [5] with length 3. It tells you the index that was used and the length of the thing indexed. Most of the time you can read the cause from those two numbers. This page covers how to read the variants, the handful of ways the bug is usually written, and how to write the guard that makes it go away.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
It belongs to the error handling guide and has a close relative in slice bounds out of range. Everything ran on Go 1.25.6.
Reading the message
package main
import "fmt"
func try(name string, f func()) {
defer func() { fmt.Printf("%-12s %v\n", name, recover()) }()
f()
}
func main() {
s := []int{10, 20, 30}
i := 5
neg := -1
var empty []int
str := "go"
try("too big", func() { _ = s[i] })
try("at length", func() { _ = s[len(s)] })
try("negative", func() { _ = s[neg] })
try("empty", func() { _ = empty[0] })
try("string", func() { _ = str[i] })
}too big runtime error: index out of range [5] with length 3
at length runtime error: index out of range [3] with length 3
negative runtime error: index out of range [-1]
empty runtime error: index out of range [0] with length 0
string runtime error: index out of range [5] with length 2The first number is the index you used, the second is the length. Indexing at exactly len(s) is the classic off-by-one: valid indexes stop at len-1. A negative index reads index out of range [-1] with no length, since no length could make it valid. The empty slice shows [0] with length 0, and a string behaves like a slice of bytes. In every case the fix lies in why the program thought the position existed.
Cause 1: off by one in a loop
The most common source is a loop condition one step too generous:
package main
import "fmt"
func main() {
defer func() { fmt.Println("recovered:", recover()) }()
items := []string{"a", "b", "c"}
for i := 0; i <= len(items); i++ { // <= visits index 3
fmt.Println(i, items[i])
}
}0 a
1 b
2 c
recovered: runtime error: index out of range [3] with length 3The loop prints three lines and then panics with index out of range [3] with length 3. The cure is <, or better, no index at all: for i, item := range items cannot go out of range, and for range n handles the counting loops. Use explicit indexes only when you need the arithmetic, and then check the boundaries at the edges of the loop, where the first and last passes use i-1 or i+1:
for i := 1; i < len(xs); i++ { // starts at 1 so xs[i-1] exists
diff := xs[i] - xs[i-1]
_ = diff
}Cause 2: the slice is empty
Code that assumes at least one element, first := items[0], panics when there are none. Empty slices come from places that look reliable: a query with no rows, a filter that matched nothing, a split of an empty string. The guard is one line, and it has to be there:
package main
import (
"fmt"
"strings"
)
func first(items []string) (string, bool) {
if len(items) == 0 {
return "", false
}
return items[0], true
}
func main() {
fmt.Println(first(nil))
fmt.Println(first([]string{"x"}))
fmt.Println(len(strings.Split("", ",")), len(strings.Fields("")))
} false
x true
1 0The last line is a surprise worth remembering: strings.Split("", ",") returns a slice with one empty string, so its length is 1, not 0, while strings.Fields("") returns an empty slice. Code that does parts := strings.Split(line, ","); name := parts[0] therefore never panics on empty input, and code that uses Fields does. Know which one you called.
Cause 3: more arguments or fields expected than present
Reading os.Args[1] with no arguments, parts[2] from a line with two fields, or matches[1] from a regular expression that did not match are all this bug, and all driven by input:
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
defer func() { fmt.Println("recovered:", recover()) }()
re := regexp.MustCompile(`id=(\d+)`)
m := re.FindStringSubmatch("no match here")
fmt.Println("matches:", len(m))
parts := strings.Split("a,b", ",")
fmt.Println("parts:", len(parts))
fmt.Println(m[1]) // m is nil: there is no group 1
}matches: 0
parts: 2
recovered: runtime error: index out of range [1] with length 0FindStringSubmatch returns nil when nothing matches, so m[1] panics with index out of range [1] with length 0. The fix is always to check the length before reading, with a message for the user in the cases where it is their input:
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: tool <file>")
os.Exit(64)
}Treat anything that comes from outside the program, arguments, files, network, user input, as having an unknown shape, and check before indexing (command line arguments shows the usual patterns).
Cause 4: a stale index after removal
An index computed before the slice changed can point past the new end. Deleting while iterating is the usual case:
package main
import "fmt"
func main() {
defer func() { fmt.Println("recovered:", recover()) }()
nums := []int{1, 2, 3, 4, 5, 6}
n := len(nums)
for i := 0; i < n; i++ { // n was computed once, before the removals
if nums[i]%2 == 0 {
nums = append(nums[:i], nums[i+1:]...)
i--
}
}
fmt.Println(nums)
}recovered: runtime error: index out of range [3] with length 3The same job done with the library, in the version that cannot panic:
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{1, 2, 3, 4, 5, 6}
nums = slices.DeleteFunc(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(nums)
}[1 3 5]The loop condition uses the original length while the slice shrinks, so a later pass reads beyond the end. Re-evaluate len(nums) in the condition, iterate backwards, or build the result without mutating, which is simplest: slices.DeleteFunc(nums, func(n int) bool { return n%2 == 0 }) does the whole thing correctly (removing an element from a slice).
Reading the trace for the real cause
The panic line names the numbers, and the stack trace names the function. Open the first frame in your own code and look at the index expression. Ask where the index came from. If it is a loop variable, check the loop bounds. If it is a function parameter, the bug may be in the caller that passed it, one frame down. If it is a computed value such as n/2 or len(a)-k, check the arithmetic for the smallest and largest inputs, because the boundary values are where it breaks: an empty slice, a one-element slice, and a length equal to k. Writing the failing input as a test before the fix keeps the bug from returning, and the table of boundary cases is usually short (testing error paths shows the shape).
What the compiler catches
An index that is a constant is checked at compile time against an array's length:
package main
import "fmt"
func main() {
var a [3]int
fmt.Println(a[5])
}# example.test/lab
./main.go:7:16: invalid argument: index 5 out of bounds [0:3]That does not run: the compiler rejects it with invalid argument: index 5 out of bounds [0:3]. Only variable indexes, and slices whose length is not known at compile time, reach the run-time check. This is why the bug appears with real data and not in the first tiny test.
Safe access patterns
Four habits remove the bug in practice.
Range instead of index whenever you only need the elements. Guard with len when you must index, and put the guard in the same function as the access. Return an ok or an error from helpers that fetch by position, so callers must handle absence. Use the library: slices.Index, slices.Contains, slices.Max and friends take care of the positions for you.
A generic accessor is a four-line helper for the case where a default is fine:
package main
import "fmt"
// At returns s[i], or def when i is out of range.
func At[T any](s []T, i int, def T) T {
if i < 0 || i >= len(s) {
return def
}
return s[i]
}
func main() {
args := []string{"prog", "run"}
fmt.Println(At(args, 1, "help"), At(args, 5, "help"))
}run helpWhen a panic is the right answer
An out-of-range index inside your own algorithm, where the index is computed and the bounds were supposed to hold, is a bug, and the panic is the correct way to say so; do not hide it with a guard that returns a default and carries on with wrong data. Guards are for input you do not control. For invariants you do control, let it panic, and let the stack trace show where the arithmetic broke (panic or error). The Go specification describes the run-time panic in its section on index expressions, and the slices package lists the helper functions.


