golang/go · warning

lzw: reader/writer is closed

Error message

lzw: reader/writer is closed

What it means

Defined as the package-level errClosed sentinel and assigned to r.err by Reader.Close(). Any subsequent Read returns it. Close is idempotent for the reader state, but the error is sticky — it does not auto-reset.

Source

Thrown at src/compress/lzw/reader.go:226

				// Undo the d.hi++ a few lines above, so that (1) we maintain
				// the invariant that d.hi < d.overflow, and (2) d.hi does not
				// eventually overflow a uint16.
				r.hi--
			} else {
				r.width++
				r.overflow = 1 << r.width
			}
		}
		if r.o >= flushBuffer {
			break
		}
	}
	// Flush pending output.
	r.toRead = r.output[:r.o]
	r.o = 0
}

var errClosed = errors.New("lzw: reader/writer is closed")

// Close closes the [Reader] and returns an error for any future read operation.
// It does not close the underlying [io.Reader].
func (r *Reader) Close() error {
	r.err = errClosed // in case any Reads come along
	return nil
}

// Reset clears the [Reader]'s state and allows it to be reused again
// as a new [Reader].
func (r *Reader) Reset(src io.Reader, order Order, litWidth int) {
	*r = Reader{}
	r.init(src, order, litWidth)
}

// NewReader creates a new [io.ReadCloser].
// Reads from the returned [io.ReadCloser] read and decompress data from r.
// If r does not also implement [io.ByteReader],

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Move the drain (io.Copy / io.ReadAll) ahead of Close, or drop the defer and close explicitly after draining.
  2. If reusing a pooled Reader, call Reset(src, order, litWidth) before each use — Reset clears the error.
  3. Check the returned error: errors.Is(err, lzw's closed sentinel) where applicable, or simply treat any read-after-close as a use-after-free bug in your code.
  4. Use sync.Pool with care: never return a closed Reader to the pool.

Example fix

// before
defer r.Close()
go func() { io.Copy(dst, r) }() // races with defer

// after: drain before close
if _, err := io.Copy(dst, r); err != nil { return err }
if err := r.Close(); err != nil { return err }
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure Read happens-before Close in your goroutine layout.
// A pre-flight check is just: track an explicit closed flag.
type safeLZW struct {
    r       *lzw.Reader
    closed  atomic.Bool
}

func (s *safeLZW) Read(p []byte) (int, error) {
    if s.closed.Load() { return 0, errors.New("reader closed") }
    return s.r.Read(p)
}

Try / catch

n, err := r.Read(buf)
if err != nil && err.Error() == "lzw: reader/writer is closed" {
    // Use-after-close bug: drain before Close, or Reset before reuse.
    return nil
}

Prevention

When it happens

Trigger: Calling Read on an lzw.Reader (or Write on an lzw.Writer) after Close has been invoked. Common with deferred Close followed by a drain loop, or when a Reader is pooled and a goroutine races ahead of the close.

Common situations: Using `defer r.Close()` and then reading in a separate goroutine; reading from a sync.Pool'd Reader that the previous owner forgot to Reset; misuse of io.Copy after Close in a cleanup path.

Related errors


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