golang/go · error

bytes.Reader.UnreadRune: at beginning of slice

Error message

bytes.Reader.UnreadRune: at beginning of slice

What it means

bytes.Reader.UnreadRune returns this error when the current position r.i is 0 (reader.go:104-106). Even if a ReadRune occurred earlier, the Reader's position-based check fires first: with no position to rewind to, the unread cannot proceed. This is distinct from the 'previous operation was not ReadRune' check, which fires when position > 0 but prevRune < 0.

Source

Thrown at src/bytes/reader.go:105

func (r *Reader) ReadRune() (ch rune, size int, err error) {
	if r.i >= int64(len(r.s)) {
		r.prevRune = -1
		return 0, 0, io.EOF
	}
	r.prevRune = int(r.i)
	if c := r.s[r.i]; c < utf8.RuneSelf {
		r.i++
		return rune(c), 1, nil
	}
	ch, size = utf8.DecodeRune(r.s[r.i:])
	r.i += int64(size)
	return
}

// UnreadRune complements [Reader.ReadRune] in implementing the [io.RuneScanner] interface.
func (r *Reader) UnreadRune() error {
	if r.i <= 0 {
		return errors.New("bytes.Reader.UnreadRune: at beginning of slice")
	}
	if r.prevRune < 0 {
		return errors.New("bytes.Reader.UnreadRune: previous operation was not ReadRune")
	}
	r.i = int64(r.prevRune)
	r.prevRune = -1
	return nil
}

// Seek implements the [io.Seeker] interface.
func (r *Reader) Seek(offset int64, whence int) (int64, error) {
	r.prevRune = -1
	var abs int64
	switch whence {
	case io.SeekStart:
		abs = offset
	case io.SeekCurrent:
		abs = r.i + offset

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Confirm the reader is not at position 0 before calling UnreadRune (check r.Len() < r.Size() or track read state).
  2. Ensure a successful ReadRune precedes each UnreadRune with no intervening Seek/Reset.
  3. Handle the start-of-input case in the parser without attempting a pushback.
  4. Use ReadRune/UnreadRune in tightly paired local blocks so the invariant is obvious.

Example fix

// before — UnreadRune at position 0
r := bytes.NewReader(data)
r.UnreadRune() // error: at beginning of slice

// after — read a rune first, then unread conditionally
_, _, err := r.ReadRune()
if err == nil && pushBack {
    r.UnreadRune()
}
Defensive patterns

Strategy: validation

Validate before calling

// Only UnreadRune when not at the beginning and after a ReadRune.
if r.Len() < int(r.Size()) { // not at start
    if err := r.UnreadRune(); err != nil { return err }
}

Try / catch

if err := r.UnreadRune(); err != nil {
    if strings.Contains(err.Error(), "at beginning of slice") {
        // no rune to push back
    } else { return err }
}

Prevention

When it happens

Trigger: Triggered by reader.UnreadRune() when r.i <= 0 (reader.go:104). Occurs at the start of the slice, after Seek to 0, after Reset, or after consuming then pushing back to the origin.

Common situations: A rune-level tokenizer that calls UnreadRune unconditionally at the top of its loop. Calling UnreadRune at the very start of input before any ReadRune. Position state corrupted by an interleaving Seek(0).

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/52320f4ef77e9307. Report an issue: GitHub.