gorilla/websocket · warning
websocket: write closed
Error message
websocket: write closed
What it means
errWriteClosed is returned by Conn.Write (and Close paths) when a write is attempted after the write side of the connection has been closed. Once the connection's write state is marked closed, further writes fail with this sentinel.
Solutions
- Stop all writers before/when closing: use a done channel or context cancellation coordinated with conn.Close()
- Treat this error as benign during shutdown — check with errors.Is and return quietly
- Use a single writer goroutine (send messages over a channel) so close ordering is deterministic
- Guard writes with a sync.Once or state flag that prevents writes after Close is invoked
Example fix
// before
conn.Close()
conn.WriteMessage(websocket.TextMessage, []byte("bye")) // errors: write closed
// after
err := conn.WriteMessage(websocket.TextMessage, []byte("bye"))
if err == nil {
conn.Close()
} Defensive patterns
Strategy: try-catch
Type guard
func isWriteClosed(err error) bool {
return errors.Is(err, websocket.ErrCloseSent) || errors.Is(err, errWriteClosedEquivalent(err))
}
// in practice: match by message since errWriteClosed is unexported
func isWriteClosedMsg(err error) bool {
return err != nil && strings.Contains(err.Error(), "websocket: write closed")
} Try / catch
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
if strings.Contains(err.Error(), "websocket: write closed") ||
errors.Is(err, websocket.ErrCloseSent) {
return nil // connection already shutting down
}
return err
} Prevention
- Signal all writer goroutines via a closed channel or context before conn.Close()
- Route every write through one owner goroutine to make close ordering deterministic
- Ignore benign shutdown-write errors instead of retrying them
When it happens
Trigger: Calling WriteMessage/NextWriter-based writes after conn.Close() has been called, or after the internal write close has occurred (e.g. following a close handshake completion or prior fatal write error).
Common situations: Background goroutines still writing after the main flow closed the connection; write pump continuing after read pump detected an error and closed; double-close of a connection followed by an attempted write.
Related errors
- concurrent write to websocket connection
- websocket: close sent
- f[1] (proxy response status text)
- http.StatusText(status)
- malformed ws or wss URL
AI-assisted analysis of gorilla/websocket@e064f32e36 (2026-08-31).
Data as JSON: /api/errors/17116ea8f5cdc6aa.
Report an issue: GitHub.
Appendix: source
Thrown at conn.go:179
// IsUnexpectedCloseError returns boolean indicating whether the error is a
// *CloseError with a code not in the list of expected codes.
func IsUnexpectedCloseError(err error, expectedCodes ...int) bool {
if e, ok := err.(*CloseError); ok {
for _, code := range expectedCodes {
if e.Code == code {
return false
}
}
return true
}
return false
}
var (
errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true}
errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()}
errBadWriteOpCode = errors.New("websocket: bad write message type")
errWriteClosed = errors.New("websocket: write closed")
errInvalidControlFrame = errors.New("websocket: invalid control frame")
)
// maskRand is an io.Reader for generating mask bytes. The reader is initialized
// to crypto/rand Reader. Tests swap the reader to a math/rand reader for
// reproducible results.
var maskRand = rand.Reader
// newMaskKey returns a new 32 bit value for masking client frames.
func newMaskKey() [4]byte {
var k [4]byte
_, _ = io.ReadFull(maskRand, k[:])
return k
}
func isControl(frameType int) bool {
return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage
}View on GitHub (pinned to e064f32e36)