← Back to DevBytes

How to Fix Go 'panic: runtime error: index out of range'

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:

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles