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
- Only call UnreadRune when the immediately preceding successful op was ReadRune; track this in parser state.
- If you need byte-level pushback after a non-rune op, use UnreadByte and re-decode the rune manually.
- Avoid interleaving Seek/UnreadByte between ReadRune and UnreadRune.
- 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
- Only call UnreadRune after a successful ReadRune with no intervening ops.
- Avoid interleaving ReadByte, Seek, or UnreadByte between ReadRune and UnreadRune.
- If pushback after a non-rune op is needed, use UnreadByte and re-decode.
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
- bytes.Reader.UnreadRune: at beginning of slice
- bytes.Reader.UnreadByte: at beginning of slice
- bytes.Buffer: UnreadRune: previous operation was not a succe
- bytes.Buffer: UnreadByte: previous operation was not a succe
- bytes.Reader.ReadAt: negative offset
AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12).
Data as JSON: /api/errors/7f85f7f7e61d26ca.
Report an issue: GitHub.