Understanding the 'index out of range' Panic in Go
A panic: runtime error: index out of range is one of the most common runtime errors in Go. It occurs when your code tries to access an element of a slice, array, or string using an index that is outside the valid boundaries. Unlike compile-time errors, this panic happens only at runtime, often in production under specific conditions that were not anticipated during development or testing.
What Is a Panic?
In Go, a panic is a sudden stop of normal execution caused by an unrecoverable error. When a goroutine panics, the runtime unwinds the stack, runs deferred functions, and if the panic is not recovered, the entire program crashes. The message index out of range is specifically triggered by illegal index operations on slices, arrays, or strings.
Why It Matters in Production
In production, an unhandled index out of range panic crashes the whole process—not just the offending request. This causes downtime, data loss, and poor user experience. Even with recovery middleware, the root cause remains and will keep causing failures until fixed. Understanding why it happens, how to analyze stack traces, and how to apply defensive coding techniques is critical for building resilient Go services.
Root Cause Analysis: Common Scenarios
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →To fix a panic, you must first understand exactly which line of code triggered it and why the index was invalid. Below are the most frequent root causes, with code examples that reproduce the panic.
1. Indexing a Slice or Array Beyond Its Length
The simplest case: you have a slice of length n, and you try to access slice[n] or higher. Remember that valid indices are 0 to len(slice)-1.
func processBatch(ids []int) {
// Suppose ids has 3 elements: [101, 102, 103]
for i := 0; i <= len(ids); i++ { // BUG: i <= len(ids) includes out-of-bounds
fmt.Println(ids[i]) // Panics on i=3
}
}
2. Indexing an Empty Slice or Nil Slice
An empty slice (len == 0) has no valid index. Attempting to access slice[0] panics. A nil slice behaves the same way—it has length 0.
func firstItem(items []string) string {
return items[0] // Panics if items is nil or empty
}
// Example usage:
var names []string
firstItem(names) // panic: index out of range [0] with length 0
3. String Index Out of Range
Strings in Go are essentially read-only byte slices. Indexing a string returns a byte, not a character. If you index beyond its length, you get the same panic. Also, using string[index] on an empty string panics.
func extractByte(s string, pos int) byte {
return s[pos] // panic if pos >= len(s)
}
// Example:
extractByte("hello", 10) // panic
extractByte("", 0) // panic
4. Slice Expressions Out of Range
The slice expression slice[a:b] also panics if a or b are outside the valid range (0 to cap(slice)). Even if the slice has sufficient capacity, b must not exceed the capacity.
func subSlice(data []int, start, end int) []int {
return data[start:end] // panic if start > len(data) or end > cap(data)
}
// Example:
arr := make([]int, 5, 5) // len=5, cap=5
subSlice(arr, 0, 6) // panic: slice bounds out of range [0:6]
5. Negative or Miscalculated Indices
Go does not support negative indexing like Python. An index like -1 is invalid. Sometimes indices are computed incorrectly, e.g., using len(slice) - 1 without checking length, or an integer overflow leads to a huge index.
func lastElement(items []int) int {
return items[len(items)-1] // panic if len(items) == 0
}
6. Concurrency and Shared Mutable Slices
In a concurrent environment, one goroutine might modify a slice (append, shrink) while another is reading. This can cause a race condition where the slice length changes between the check and the access, leading to an index out of range panic.
var sharedData []int
func writer() {
for i := 0; i < 100; i++ {
sharedData = append(sharedData, i)
}
}
func reader() {
// No synchronization — may read len while writer is appending
if len(sharedData) > 0 {
fmt.Println(sharedData[len(sharedData)-1]) // possible panic
}
}
How to Fix It in Production: Immediate Mitigation
Before diving into the root cause, you need to prevent the panic from crashing your entire service. This is done via recover, typically in a middleware or a deferred function in the main goroutine. However, recovery alone is not a fix—it's a safety net.
Using Recover to Prevent Crash
A common pattern in HTTP servers is to wrap each handler with a recovery middleware. For non-HTTP applications, you can place a deferred recover() call at the top of critical goroutines.
func safeHandler(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v\nstack trace: %s", err, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
fn(w, r)
}
}
// Usage:
http.HandleFunc("/data", safeHandler(dataEndpoint))
The debug.Stack() call captures the full stack trace, which is crucial for root cause analysis. Log it immediately to a durable log stream.
Logging and Alerting
When a panic is recovered, treat it as a critical incident. Log the entire stack trace, the request context, and any relevant variable values. Set up alerts so that the team can investigate. Never silently swallow panics—they indicate a bug that must be fixed.
How to Fix It: Root Cause Resolution
Once you have the stack trace and the panic location, you can pinpoint the exact line and the faulty index. The fix almost always involves adding a bounds check or switching to a safer iteration pattern.
Bounds Checking Before Indexing
The most straightforward fix: verify that the index is within [0, len(slice)) before accessing the element. Return an error or fallback value if out of bounds.
func safeAccess(items []string, index int) (string, error) {
if index < 0 || index >= len(items) {
return "", fmt.Errorf("index %d out of range (len=%d)", index, len(items))
}
return items[index], nil
}
Leveraging the Range Loop
Whenever possible, use range to iterate over slices and arrays. It automatically handles bounds and eliminates the risk of off-by-one errors.
func processBatchSafe(ids []int) {
for _, id := range ids { // No index arithmetic, no panic risk
fmt.Println(id)
}
}
If you need the index, for i, v := range slice gives a guaranteed valid index.
Safely Accessing the First or Last Element
Many panics occur in helper functions that grab the first or last element. Always check length first.
func lastElement(items []int) (int, bool) {
if len(items) == 0 {
return 0, false
}
return items[len(items)-1], true
}
func firstElement(items []int) (int, bool) {
if len(items) == 0 {
return 0, false
}
return items[0], true
}
Safe Slice Expressions
For sub-slicing, validate the bounds against both length and capacity. If you only need a sub-slice up to the available length, clamp the end index.
func safeSlice(data []int, start, end int) ([]int, error) {
if start < 0 || start > len(data) {
return nil, fmt.Errorf("start index out of range")
}
if end > cap(data) {
return nil, fmt.Errorf("end index exceeds capacity")
}
return data[start:end], nil
}
Handling String Indexing Properly
String indexing returns bytes, not runes. For multi-byte characters, use for _, r := range s to iterate runes, or convert to []rune if you need rune-level indexing. Always check length before indexing a string.
func safeByteAt(s string, idx int) (byte, error) {
if idx < 0 || idx >= len(s) {
return 0, fmt.Errorf("index out of range")
}
return s[idx], nil
}
Using Helper Functions and Libraries
Consider creating a small utility package with safe accessors for common operations. For example, a SafeSlice type that encapsulates a slice and provides methods with bounds checking.
type SafeSlice[T any] struct {
data []T
}
func (s *SafeSlice[T]) At(index int) (T, error) {
if index < 0 || index >= len(s.data) {
var zero T
return zero, fmt.Errorf("index %d out of range", index)
}
return s.data[index], nil
}
func (s *SafeSlice[T]) First() (T, bool) {
if len(s.data) == 0 {
var zero T
return zero, false
}
return s.data[0], true
}
Best Practices to Prevent Index Out of Range
- Always check length before indexing: Get into the habit of writing
if len(slice) > idxguards, especially when the index is a variable or comes from user input. - Prefer range loops over index loops: They are inherently safe and more readable.
- Validate indices early: If a function accepts an index parameter, validate it at the top of the function and return an error for invalid values.
- Use static analysis and linters: Tools like
staticcheckandgo vetcan detect some obvious out-of-bounds issues. Integrate them into your CI pipeline. - Write tests for edge cases: Include empty slices, nil slices, negative indices, and indices equal to length in your test cases.
- Protect shared state with synchronization: Use mutexes or channels to prevent concurrent modification of slices that are accessed from multiple goroutines.
- Monitor and alert on recovered panics: Every recovered panic should trigger an alert so that developers can fix the underlying bug.
- Log enough context: When a panic occurs, log the full stack trace, the values of relevant variables (especially the slice length and the offending index), and the request identifier.
Conclusion
The panic: runtime error: index out of range in Go is a symptom of a missing bounds check, a logical off-by-one mistake, or a concurrency race. In production, it can bring down an entire service, but with proper recovery mechanisms you can limit the damage and capture the essential debugging information. The real fix lies in root cause analysis: using the stack trace to locate the faulty index operation, understanding why the index became invalid, and then applying defensive coding patterns—bounds checks, range loops, and safe accessor helpers. By adopting these practices and embedding them into your development workflow, you can eliminate index-out-of-range panics and build Go applications that are robust and production-ready.