golang/go · error

bytes.Reader.UnreadRune: previous operation was not ReadRune

Error message

bytes.Reader.UnreadRune: previous operation was not ReadRune

What it means

bytes.Reader.UnreadRune returns this error when r.prevRune < 0 (reader.go:107-109), meaning the immediately preceding operation was not a successful ReadRune. prevRune is set to the rune's start index only by ReadRune; Read, ReadByte, Seek, UnreadByte, and Reset all set it to -1. So this fires when position > 0 (the at-beginning check passed) but the last op was not ReadRune.

Source

Thrown at src/bytes/reader.go:108

		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
	case io.SeekEnd:
		abs = int64(len(r.s)) + offset
	default:

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Only call UnreadRune when the immediately preceding successful op was ReadRune; track this in parser state.
  2. If you need byte-level pushback after a non-rune op, use UnreadByte and re-decode the rune manually.
  3. Avoid interleaving Seek/UnreadByte between ReadRune and UnreadRune.
  4. Restructure so ReadRune and UnreadRune are paired locally with no intervening reader calls.

Example fix

// before — UnreadRune after ReadByte fails
b, _ := r.ReadByte()
r.UnreadRune() // error: previous op was not ReadRune

// after — use ReadRune, or push back at byte level
r2, _, _ := r.ReadRune()
if pushBack { r.UnreadRune() }
// or:
b, _ := r.ReadByte()
if pushBack { r.UnreadByte() }
Defensive patterns

Strategy: validation

Validate before calling

// Track whether the last op was ReadRune before calling UnreadRune.
type tracked struct{ r *bytes.Reader; lastWasRune bool }
func (t *tracked) ReadRune() (rune, int, error) {
    ch, sz, err := t.r.ReadRune()
    t.lastWasRune = (err == nil)
    return ch, sz, err
}
func (t *tracked) UnreadRune() error {
    if !t.lastWasRune { return errors.New("last op was not ReadRune") }
    err := t.r.UnreadRune(); t.lastWasRune = false; return err
}

Try / catch

if err := r.UnreadRune(); err != nil {
    if strings.Contains(err.Error(), "previous operation was not ReadRune") {
        // use UnreadByte + manual decode instead
    } else { return err }
}

Prevention

When it happens

Trigger: Triggered by reader.UnreadRune() when r.i > 0 and r.prevRune < 0 (reader.go:107). Common after ReadByte, after Read, after Seek (which clears prevRune at reader.go:117), or after UnreadByte (which clears prevRune at reader.go:81).

Common situations: Calling UnreadRune after ReadByte in a mixed byte/rune parser. Calling UnreadRune after a Seek that repositioned the reader. Calling UnreadRune after UnreadByte — the second pushback clears the rune context.

Related errors


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