Skip to main content

Echo WebSocket Error Handling

When building real-time applications with Echo WebSockets, effective error handling is critical for maintaining stable connections and providing a good user experience. This guide will walk you through various error handling strategies and best practices for Echo WebSockets.

Introduction to WebSocket Error Handling

WebSockets provide persistent connections between clients and servers, making them ideal for real-time applications. However, these long-lived connections can encounter various issues:

  • Connection drops
  • Timeouts
  • Invalid messages
  • Authentication failures
  • Server-side errors

Proper error handling ensures your application can gracefully manage these issues, recover when possible, and provide meaningful feedback to users.

Common WebSocket Errors

Before we dive into implementation, let's understand the common types of errors in WebSocket applications:

  1. Connection Errors: Failed initial connections or disconnections
  2. Protocol Errors: Invalid message formats or unexpected messages
  3. Application Errors: Logical errors in processing WebSocket messages
  4. Authentication Errors: Issues with user permissions or authentication
  5. System Errors: Resource limitations or server crashes

Basic Error Handling in Echo WebSockets

Let's start with a simple Echo WebSocket handler that includes error handling:

go
package main

import (
"fmt"
"net/http"

"github.com/labstack/echo/v4"
"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Allow all connections in this example
},
}

func handleWebSocket(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Could not upgrade to WebSocket: " + err.Error(),
})
}
defer ws.Close()

// Error handling for read operations
for {
messageType, message, err := ws.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure) {
fmt.Printf("WebSocket read error: %v\n", err)
}
break // Exit the loop on any read error
}

// Echo the message back
if err := ws.WriteMessage(messageType, message); err != nil {
fmt.Printf("WebSocket write error: %v\n", err)
break
}
}

return nil
}

func main() {
e := echo.New()
e.GET("/ws", handleWebSocket)
e.Start(":8080")
}

In this example:

  • We handle the initial connection upgrade error
  • We use websocket.IsUnexpectedCloseError() to differentiate between normal and abnormal closures
  • We log errors for debugging purposes
  • We gracefully exit the WebSocket loop when errors occur

Client-Side Error Handling

WebSocket error handling isn't just server-side. Here's how you might handle errors in a JavaScript client:

javascript
const connectWebSocket = () => {
const socket = new WebSocket('ws://localhost:8080/ws');

// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected to WebSocket server');
});

// Listen for errors
socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});

// Connection closed
socket.addEventListener('close', (event) => {
console.log(`Connection closed: ${event.code} ${event.reason}`);

// Implement reconnection logic if needed
if (event.code !== 1000) { // Normal closure
console.log('Attempting to reconnect in 5 seconds...');
setTimeout(connectWebSocket, 5000);
}
});

// Listen for messages
socket.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data);
console.log('Message from server:', data);
} catch (e) {
console.error('Error parsing message:', e);
}
});

return socket;
};

// Start connection
const socket = connectWebSocket();

This client code:

  • Handles connection errors
  • Implements reconnection logic
  • Gracefully handles message parsing errors
  • Differentiates between normal and abnormal closures

Advanced Error Handling Techniques

1. Custom Error Responses

For more sophisticated error handling, you can define custom error responses:

go
// Custom error response structure
type WSError struct {
Code int `json:"code"`
Message string `json:"message"`
}

// Send a structured error message
func sendErrorMessage(ws *websocket.Conn, code int, message string) error {
errMsg := WSError{
Code: code,
Message: message,
}

return ws.WriteJSON(errMsg)
}

// Usage example in handler
if invalidMessage {
if err := sendErrorMessage(ws, 4001, "Invalid message format"); err != nil {
// Handle write error
return
}
}

2. Connection State Management

Tracking connection state helps manage errors more effectively:

go
type Connection struct {
ws *websocket.Conn
isConnected bool
mu sync.Mutex
}

func (c *Connection) SafeWrite(messageType int, data []byte) error {
c.mu.Lock()
defer c.mu.Unlock()

if !c.isConnected {
return errors.New("connection is closed")
}

return c.ws.WriteMessage(messageType, data)
}

func (c *Connection) Close() {
c.mu.Lock()
defer c.mu.Unlock()

if c.isConnected {
c.ws.Close()
c.isConnected = false
}
}

3. Implementing Ping/Pong for Connection Health

Regular ping/pong exchanges help detect dead connections:

go
func setupPingPong(ws *websocket.Conn) {
// Set pong handler
ws.SetPongHandler(func(string) error {
ws.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})

// Send periodic pings
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
fmt.Printf("Ping error: %v\n", err)
return
}
}
}
}()
}

Real-World Example: Chat Application with Error Handling

Let's build a more comprehensive example of a chat application with robust error handling:

go
package main

import (
"fmt"
"log"
"net/http"
"sync"
"time"

"github.com/labstack/echo/v4"
"github.com/gorilla/websocket"
)

var (
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

// Store active clients
clients = make(map[*websocket.Conn]bool)
clientsMu sync.Mutex
)

// Message structure
type Message struct {
Type string `json:"type"`
Content string `json:"content"`
Sender string `json:"sender"`
}

func handleChatWebSocket(c echo.Context) error {
username := c.QueryParam("username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}

ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
log.Printf("WebSocket upgrade error: %v", err)
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Could not upgrade to WebSocket",
})
}

// Register new client
clientsMu.Lock()
clients[ws] = true
clientsMu.Unlock()

// Clean up on disconnect
defer func() {
ws.Close()
clientsMu.Lock()
delete(clients, ws)
clientsMu.Unlock()

// Notify others that this user has left
broadcastMessage(Message{
Type: "system",
Content: username + " has left the chat",
})
}()

// Setup ping/pong
setupPingPong(ws)

// Send welcome message
welcomeMsg := Message{
Type: "system",
Content: "Welcome to the chat, " + username,
}
if err := ws.WriteJSON(welcomeMsg); err != nil {
log.Printf("Welcome message error: %v", err)
return nil
}

// Notify others about new user
broadcastMessage(Message{
Type: "system",
Content: username + " has joined the chat",
})

// Handle incoming messages
for {
var msg Message
err := ws.ReadJSON(&msg)
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure) {
log.Printf("WebSocket read error: %v", err)
}
break
}

// Validate message
if msg.Content == "" {
// Send error back to this client only
errorMsg := Message{
Type: "error",
Content: "Message cannot be empty",
}
ws.WriteJSON(errorMsg)
continue
}

// Add sender information
msg.Sender = username

// Broadcast message to all clients
broadcastMessage(msg)
}

return nil
}

func broadcastMessage(msg Message) {
clientsMu.Lock()
defer clientsMu.Unlock()

// Send message to all connected clients
for client := range clients {
err := client.WriteJSON(msg)
if err != nil {
log.Printf("Broadcast error: %v", err)
client.Close()
delete(clients, client)
}
}
}

func setupPingPong(ws *websocket.Conn) {
// Set initial deadline
ws.SetReadDeadline(time.Now().Add(60 * time.Second))

// Configure pong handler
ws.SetPongHandler(func(string) error {
ws.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})

// Send periodic pings
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()

for range ticker.C {
if err := ws.WriteControl(
websocket.PingMessage,
[]byte{},
time.Now().Add(10*time.Second),
); err != nil {
return
}
}
}()
}

func main() {
e := echo.New()
e.GET("/ws/chat", handleChatWebSocket)
e.Static("/", "public")

log.Println("Server starting on :8080")
e.Start(":8080")
}

For client-side implementation, you would need an HTML page with JavaScript:

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Echo WebSocket Chat</title>
<style>
#chat-box {
height: 300px;
border: 1px solid #ccc;
padding: 10px;
overflow-y: scroll;
margin-bottom: 10px;
}
.error { color: red; }
.system { color: blue; font-style: italic; }
</style>
</head>
<body>
<h1>Echo WebSocket Chat</h1>

<div id="login-form">
<input type="text" id="username" placeholder="Enter your username">
<button onclick="login()">Join Chat</button>
</div>

<div id="chat-container" style="display: none;">
<div id="chat-box"></div>

<div id="message-form">
<input type="text" id="message" placeholder="Type a message...">
<button onclick="sendMessage()">Send</button>
</div>

<div id="connection-status">Status: Disconnected</div>
<button onclick="reconnect()">Reconnect</button>
</div>

<script>
let socket;
let username;

function login() {
username = document.getElementById('username').value.trim();
if (!username) {
alert('Please enter a username');
return;
}

document.getElementById('login-form').style.display = 'none';
document.getElementById('chat-container').style.display = 'block';

connectWebSocket();
}

function connectWebSocket() {
updateStatus('Connecting...');

socket = new WebSocket(`ws://${window.location.host}/ws/chat?username=${encodeURIComponent(username)}`);

socket.addEventListener('open', (event) => {
updateStatus('Connected');
});

socket.addEventListener('message', (event) => {
try {
const message = JSON.parse(event.data);
displayMessage(message);
} catch (e) {
console.error('Error parsing message:', e);
displayError('Failed to parse message from server');
}
});

socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
updateStatus('Error occurred');
displayError('Connection error occurred');
});

socket.addEventListener('close', (event) => {
updateStatus('Disconnected');

if (event.code !== 1000) {
displayError(`Connection closed: ${event.reason || 'Server disconnected'}`);

// Auto-reconnect after 5 seconds
setTimeout(() => {
if (confirm('Connection lost. Reconnect?')) {
reconnect();
}
}, 5000);
}
});
}

function sendMessage() {
const messageInput = document.getElementById('message');
const content = messageInput.value.trim();

if (!content) return;

if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: 'chat',
content: content
}));
messageInput.value = '';
} else {
displayError('Not connected to server');
}
}

function reconnect() {
if (socket) {
socket.close();
}
connectWebSocket();
}

function displayMessage(message) {
const chatBox = document.getElementById('chat-box');
const messageDiv = document.createElement('div');

if (message.type === 'error') {
messageDiv.className = 'error';
messageDiv.textContent = `Error: ${message.content}`;
} else if (message.type === 'system') {
messageDiv.className = 'system';
messageDiv.textContent = message.content;
} else {
messageDiv.textContent = `${message.sender}: ${message.content}`;
}

chatBox.appendChild(messageDiv);
chatBox.scrollTop = chatBox.scrollHeight;
}

function displayError(message) {
const chatBox = document.getElementById('chat-box');
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = message;
chatBox.appendChild(errorDiv);
chatBox.scrollTop = chatBox.scrollHeight;
}

function updateStatus(status) {
document.getElementById('connection-status').textContent = `Status: ${status}`;
}

// Handle page closing
window.addEventListener('beforeunload', () => {
if (socket) {
// Close code 1000 indicates a normal closure
socket.close(1000, 'Page closed');
}
});
</script>
</body>
</html>

Best Practices for WebSocket Error Handling

  1. Use Structured Error Messages

    • Define consistent error formats for clients to parse
  2. Implement Reconnection Strategies

    • Both client and server should handle reconnection gracefully
  3. Monitor Connection Health

    • Use ping/pong messages to detect dead connections
  4. Graceful Degradation

    • Fall back to HTTP polling if WebSocket connections fail
  5. Comprehensive Logging

    • Log both client and server errors for debugging
  6. Rate Limiting

    • Prevent clients from flooding the server with messages
  7. Authentication Error Handling

    • Provide clear feedback for authentication failures
  8. Proper Resource Cleanup

    • Close connections and release resources when errors occur

Common WebSocket Error Codes

Understanding WebSocket close codes helps in better error handling:

CodeNameDescription
1000Normal ClosureNormal closure, connection completed successfully
1001Going AwayEndpoint is going away (server shutdown)
1002Protocol ErrorError in the WebSocket protocol
1003Unsupported DataReceived data of a type it cannot accept
1008Policy ViolationMessage violates policy
1011Internal ErrorServer encountered unexpected condition

Summary

Effective error handling is crucial for robust WebSocket applications. By implementing proper error detection, meaningful error messages, and recovery mechanisms, you can create a more reliable real-time application.

In this guide, we covered:

  1. Basic WebSocket error handling in Echo
  2. Client-side error management
  3. Advanced techniques like connection state tracking and ping/pong
  4. A complete chat application example with comprehensive error handling
  5. Best practices for robust WebSocket applications

With these strategies in place, your Echo WebSocket applications will be more resilient and provide a better user experience even when issues occur.

Additional Resources

Exercises

  1. Modify the chat example to implement a reconnection backoff strategy (increasing delays between reconnection attempts)
  2. Add server-side message validation with specific error codes for different validation failures
  3. Implement a WebSocket connection status dashboard that displays all active connections and their health
  4. Create a fallback mechanism that switches to HTTP polling if WebSocket connections fail repeatedly


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)