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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)