Understanding the "panic: runtime error: index out of range" Error
One of the most common runtime panics in Go is panic: runtime error: index out of range. It stops your program dead in its tracks and can be frustrating to debug if you don't know exactly what causes it. This tutorial covers everything you need to know: what this panic means, why it happens, how to fix it with practical code examples, and best practices to avoid it entirely.
What Is This Panic?
In Go, an "index out of range" panic occurs when you try to access an element of a slice, array, or string using an index that is less than zero or greater than or equal to the length of the collection. The Go runtime checks bounds on every indexed access and immediately panics if the index is invalid. There is no graceful fallback — your program crashes unless you have a deferred recover() call somewhere in the call stack.
The error message typically looks like this:
panic: runtime error: index out of range [X] with length Y
goroutine 1 [running]:
main.main()
/path/to/file.go:42 +0xabc
The bracketed numbers give you critical clues: [X] is the invalid index you attempted to use, and Y is the actual length of the collection at the moment of the panic.
Why It Matters
Index out of range panics matter because they represent a fundamental logic flaw in your program. Unlike compile-time errors, these happen at runtime and can surface in production under edge conditions — empty inputs, unexpected data sizes, or race conditions. A single unhandled panic can take down an entire web server or processing pipeline. Understanding how to prevent these panics is essential for writing robust, production-grade Go code.
Common Scenarios That Trigger This Panic
Let's examine the most frequent situations where developers encounter this panic, with complete code examples that demonstrate the problem.
Slicing Beyond Capacity
Slicing a slice with a high index that exceeds its length or capacity triggers the panic. This often happens when you assume a slice has more elements than it actually does.
package main
import "fmt"
func main() {
data := []int{10, 20, 30}
// Length is 3, valid indices are 0, 1, 2
// Attempting to slice beyond length — this panics:
// panic: runtime error: slice bounds out of range [10:20] with length 3
chunk := data[10:20]
fmt.Println(chunk)
}
Accessing Array Elements Out of Bounds
Arrays in Go have fixed sizes. Accessing an index greater than or equal to the array length causes an immediate panic.
package main
import "fmt"
func main() {
arr := [3]string{"apple", "banana", "cherry"}
// Valid: arr[0] through arr[2]
fmt.Println(arr[2]) // Output: cherry
// Panics: index 3 is out of bounds for array of length 3
// panic: runtime error: index out of range [3] with length 3
fmt.Println(arr[3])
}
String Indexing Gone Wrong
Strings in Go are essentially read-only slices of bytes. Indexing a string returns a byte, not a rune. If you index beyond the string length, you get the panic. Additionally, indexing into a string with multi-byte characters requires special care.
package main
import "fmt"
func main() {
str := "hello"
// Length is 5 bytes, valid indices: 0 to 4
fmt.Println(str[4]) // Output: 111 (ASCII value of 'o')
// Panics: index 5 is out of bounds
// panic: runtime error: index out of range [5] with length 5
fmt.Println(str[5])
}
Empty Slice or Nil Slice Access
An empty or nil slice has length 0. Any attempt to access an element by index — including index 0 — will panic.
package main
import "fmt"
func main() {
var emptySlice []int // nil slice, length 0
// Panics: panic: runtime error: index out of range [0] with length 0
fmt.Println(emptySlice[0])
madeSlice := make([]int, 0) // empty slice, length 0
// Also panics with the same error
fmt.Println(madeSlice[0])
}
How to Fix It — Practical Solutions
Now let's go through the techniques you can use to fix and prevent index out of range panics in your Go code.
Always Check Length Before Indexing
The most direct fix is to guard every indexed access with a length check. This is simple and explicit.
package main
import (
"fmt"
"os"
)
func getFirstElement(data []int) int {
if len(data) == 0 {
fmt.Println("Warning: slice is empty, returning default")
return 0 // or return an error, depending on your design
}
return data[0]
}
func main() {
items := []int{42, 84, 126}
fmt.Println(getFirstElement(items)) // Output: 42
empty := []int{}
fmt.Println(getFirstElement(empty)) // Output: Warning... 0
// This would have panicked without the check:
// fmt.Println(empty[0])
}
Use Range Loops to Avoid Manual Indexing
The range keyword iterates over a slice, array, or string safely, never exceeding bounds. Whenever possible, prefer range over manual index manipulation.
package main
import "fmt"
func printWithIndices(data []string) {
// Manual indexing — risky if you miscalculate
// for i := 0; i <= len(data); i++ { // BUG: <= causes panic on last iteration
// fmt.Println(i, data[i])
// }
// Safe: range handles bounds automatically
for i, val := range data {
fmt.Printf("Index %d: %s\n", i, val)
}
}
func main() {
fruits := []string{"kiwi", "mango", "papaya"}
printWithIndices(fruits)
// Also works perfectly with empty slices
empty := []string{}
printWithIndices(empty) // No output, no panic
}
Safely Slicing with Bounds Checks
When you need to take a sub-slice using expressions like data[start:end], validate both start and end against the slice length. If start or end might exceed the length, clamp them or return an error.
package main
import (
"fmt"
"errors"
)
func safeSlice(data []int, start, end int) ([]int, error) {
if start < 0 || end < 0 {
return nil, errors.New("negative indices are not allowed")
}
if start > len(data) {
return nil, fmt.Errorf("start index %d exceeds slice length %d", start, len(data))
}
if end > len(data) {
// Clamp end to the slice length instead of panicking
end = len(data)
}
if start > end {
return nil, fmt.Errorf("start index %d is greater than end index %d", start, end)
}
return data[start:end], nil
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
result, err := safeSlice(numbers, 1, 4)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Slice:", result) // Output: Slice: [2 3 4]
}
// This would panic with data[10:20] but now returns an error
result, err = safeSlice(numbers, 10, 20)
if err != nil {
fmt.Println("Error:", err) // Output: Error: start index 10 exceeds slice length 5
}
}
Defensive Programming with Nil and Empty Checks
Nil slices behave like empty slices in terms of length (both have len() == 0), but a nil slice cannot be indexed. Always treat nil and empty slices identically by checking len() == 0 before indexing.
package main
import "fmt"
type Config struct {
Tags []string
}
func processConfig(cfg *Config) {
// Defensive check: handle nil Config and nil/empty Tags
if cfg == nil || len(cfg.Tags) == 0 {
fmt.Println("No tags available, using defaults")
return
}
fmt.Println("First tag:", cfg.Tags[0])
}
func main() {
// Case 1: nil Config pointer
processConfig(nil) // Output: No tags available...
// Case 2: Config with nil Tags slice
cfg2 := &Config{Tags: nil}
processConfig(cfg2) // Output: No tags available...
// Case 3: Valid Config
cfg3 := &Config{Tags: []string{"production", "critical"}}
processConfig(cfg3) // Output: First tag: production
}
Using Copy to Avoid Shared Array Issues
Sometimes an index out of range panic occurs because you're working with a slice that shares an underlying array with another slice, and you assume the capacity is larger than it actually is after appending. Using copy to create independent slices avoids this class of bugs.
package main
import "fmt"
func main() {
// Original array with capacity for growth
original := make([]int, 3, 10)
original[0], original[1], original[2] = 10, 20, 30
// Create a slice that shares the backing array
shared := original[:2] // length 2, capacity 10
// Appending to original changes the backing array
original = append(original, 40, 50)
// shared still has length 2 — indexing shared[2] would panic
if len(shared) < 3 {
fmt.Println("shared slice is too short for index 2")
}
// Solution: use copy to create an independent slice
independent := make([]int, len(original))
copy(independent, original)
// Now independent has its own backing array with accurate length
fmt.Println(independent[3]) // Output: 40
}
Best Practices to Prevent Index Out of Range Panics
Here is a concise list of best practices you should adopt to minimize the risk of index out of range panics in your Go projects:
- Prefer
rangeover manual indexing — It eliminates off-by-one errors and works safely on empty collections. - Check
len()before any indexed access — Especially on function parameters, user inputs, or data from external sources. - Handle nil slices explicitly — Remember that
len(nilSlice)returns 0, so a nil check alone isn't enough; check length. - Validate slice bounds before slicing — When using
data[start:end], ensure both values are within0tolen(data). - Avoid hard-coded indices — Constants like
data[5]assume the slice is always at least 6 elements long, which may not hold true. - Use helper functions for safe access — Create utility functions like
safeGet(arr, index, fallback)for frequently accessed patterns. - Write unit tests for edge cases — Test your functions with empty slices, nil slices, single-element slices, and slices at exact capacity boundaries.
- Consider using
recover()in top-level goroutines — For long-running services, a deferred recover can log the panic and keep the service alive. - Enable race detection during development — Run
go test -raceto catch concurrent slice mutations that might cause unexpected lengths.
Safe Access Utility Example
Here is a reusable utility function that encapsulates the bounds-checking pattern for any comparable type using Go generics (Go 1.18+):
package main
import "fmt"
// SafeGet returns the element at index i from slice s, or fallback if out of bounds.
func SafeGet[T any](s []T, i int, fallback T) T {
if i >= 0 && i < len(s) {
return s[i]
}
return fallback
}
// SafeSlice returns s[start:end] with bounds clamping, never panics.
func SafeSlice[T any](s []T, start, end int) []T {
length := len(s)
if start < 0 {
start = 0
}
if end < 0 {
end = 0
}
if start > length {
start = length
}
if end > length {
end = length
}
if start > end {
start = end
}
return s[start:end]
}
func main() {
data := []int{100, 200, 300}
// Safe indexed access
val := SafeGet(data, 5, -1)
fmt.Println("SafeGet at index 5:", val) // Output: -1 (fallback)
val = SafeGet(data, 1, -1)
fmt.Println("SafeGet at index 1:", val) // Output: 200
// Safe slicing
chunk := SafeSlice(data, 1, 10)
fmt.Println("SafeSlice(1,10):", chunk) // Output: [200 300] (clamped to length)
chunk = SafeSlice(data, 5, 10)
fmt.Println("SafeSlice(5,10):", chunk) // Output: [] (empty, no panic)
// Works with empty slices
empty := []int{}
fmt.Println("SafeGet on empty:", SafeGet(empty, 0, 99)) // Output: 99
fmt.Println("SafeSlice on empty:", SafeSlice(empty, 0, 3)) // Output: []
}
Conclusion
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →The panic: runtime error: index out of range error in Go is entirely preventable with disciplined bounds checking and defensive programming habits. By understanding that this panic stems from accessing slices, arrays, or strings at invalid indices, you can systematically eliminate it from your codebase. Adopt range loops as your default iteration pattern, always validate lengths before indexing, handle nil and empty collections gracefully, and use safe access utilities for complex slice operations. With these practices in place, your Go programs will be robust, predictable, and free from one of the most common runtime crashes developers face.