WebSocket API Reference
Quick Reference
Creating a Connection
const socket = new WebSocket('wss://echo.websocket.org');
Essential Events
socket.onopen = (event) => { /* Connection established */};socket.onmessage = (event) => { /* Message received */};socket.onerror = (event) => { /* Error occurred */};socket.onclose = (event) => { /* Connection closed */};
Sending Data
socket.send('Hello server!'); // Textsocket.send(binaryData.buffer); // Binary
Closing Connection
socket.close(1000, 'Normal closure');
The WebSocket API Overview
The WebSocket API is an advanced technology that enables persistent, bidirectional, full-duplex communication channels between web clients and servers. Unlike traditional HTTP requests, WebSocket connections remain open, allowing for real-time data exchange without the overhead of HTTP polling.
Key Benefits
- Full-duplex communication: Both client and server can send messages independently
- Low latency: No HTTP overhead for each message
- Persistent connection: Maintains state between messages
- Event-driven: Asynchronous, non-blocking communication model
- Binary and text support: Efficient transmission of different data types
The WebSocket Interface
The WebSocket interface is the primary API for connecting to a WebSocket server and exchanging data. It follows an asynchronous, event-driven programming model where events are fired as the connection state changes and data is received.
Constructor
const socket = new WebSocket(url[, protocols]);
Parameters
url
(required): The WebSocket server URL- Must use
ws://
for unencrypted orwss://
for encrypted connections - Can include path and query parameters
- Must use
protocols
(optional): Subprotocol selection- Single string or array of strings
- Server selects one from the provided list
- Useful for versioning or feature negotiation
Examples
// Basic connection to secure WebSocket serverconst socket = new WebSocket('wss://echo.websocket.org');
// Connection with path and query parametersconst socket = new WebSocket('wss://api.example.com/v2/stream?token=abc123');
// Connection with single subprotocolconst socket = new WebSocket('wss://game.example.com', 'game-protocol-v2');
// Connection with multiple subprotocol optionsconst socket = new WebSocket('wss://chat.example.com', ['chat-v2', 'chat-v1']);
Connection States
The WebSocket connection progresses through several states during its lifecycle:
State | Value | Constant | Description |
---|---|---|---|
CONNECTING | 0 | WebSocket.CONNECTING | Connection not yet established |
OPEN | 1 | WebSocket.OPEN | Connection established and ready |
CLOSING | 2 | WebSocket.CLOSING | Connection closing handshake initiated |
CLOSED | 3 | WebSocket.CLOSED | Connection closed or failed |
Checking Connection State
function sendMessage(socket, data) { switch (socket.readyState) { case WebSocket.CONNECTING: // Queue message for when connection opens console.log('Still connecting...'); break; case WebSocket.OPEN: // Safe to send socket.send(data); break; case WebSocket.CLOSING: case WebSocket.CLOSED: // Connection unavailable console.log('Connection is closed'); break; }}
Properties
Read-Only Properties
url
Returns the absolute URL of the WebSocket connection.
console.log(socket.url); // "wss://echo.websocket.org/"
protocol
Returns the selected subprotocol, if any.
console.log(socket.protocol); // "chat-v2" or empty string
readyState
Returns the current connection state (0-3).
if (socket.readyState === WebSocket.OPEN) { socket.send('Ready to communicate!');}
bufferedAmount
Returns bytes queued but not yet transmitted. Useful for backpressure handling.
if (socket.bufferedAmount > 1024 * 1024) { // 1MB threshold // Too much data queued, pause sending console.warn('Buffer full, pausing sends');}
extensions
Returns negotiated extensions (e.g., compression).
console.log(socket.extensions); // "permessage-deflate; client_max_window_bits"
Configurable Properties
binaryType
Controls how binary data is exposed to JavaScript.
socket.binaryType = 'arraybuffer'; // Default, recommended// orsocket.binaryType = 'blob'; // For file-like data
Methods
send(data)
Transmits data to the server. Queues data if connection is still opening.
Parameters
data
: String, ArrayBuffer, Blob, or ArrayBufferView
Examples
// Text messagesocket.send('Hello, server!');
// JSON messagesocket.send(JSON.stringify({ type: 'chat', message: 'Hi!' }));
// Binary dataconst buffer = new ArrayBuffer(8);const view = new DataView(buffer);view.setInt32(0, 42);socket.send(buffer);
// Typed arrayconst bytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);socket.send(bytes);
close([code][, reason])
Initiates the closing handshake or closes the connection.
Parameters
code
(optional): Status code (default: 1000)reason
(optional): Human-readable close reason (max 123 bytes UTF-8)
Examples
// Normal closuresocket.close();
// With code and reasonsocket.close(1000, 'Work complete');
// Custom application code (4000-4999 range)socket.close(4001, 'Idle timeout');
Events
WebSocket programming follows an asynchronous, event-driven model. Events can be
handled using onevent
properties or addEventListener()
.
Event Types
The WebSocket API supports four event types:
open
: Connection establishedmessage
: Data received from servererror
: Error occurred (connection failures, protocol errors)close
: Connection closed
Important Note: In JavaScript, WebSocket events can be handled using
“onevent” properties (like onopen
) or using addEventListener()
. Either
approach works, but addEventListener()
allows multiple handlers for the same
event.
open
Event
Fired when the WebSocket connection is successfully established.
// Using onevent propertysocket.onopen = function (event) { console.log('Connected to server'); console.log('Protocol:', socket.protocol); console.log('Extensions:', socket.extensions);
// Connection is ready, safe to send socket.send('Hello, server!');};
// Using addEventListener (allows multiple handlers)socket.addEventListener('open', function (event) { console.log('WebSocket ready state:', socket.readyState); // Will be 1 (OPEN)});
message
Event
Fired when data is received from the server. The event.data
contains the
message payload.
socket.onmessage = function (event) { // Check data type if (typeof event.data === 'string') { // Text message console.log('Text message:', event.data);
// Parse JSON if expected try { const json = JSON.parse(event.data); processMessage(json); } catch (e) { processText(event.data); } } else if (event.data instanceof ArrayBuffer) { // Binary message (when binaryType = 'arraybuffer') const view = new DataView(event.data); console.log('Binary message, first byte:', view.getUint8(0)); } else if (event.data instanceof Blob) { // Binary message (when binaryType = 'blob') event.data.arrayBuffer().then((buffer) => { processArrayBuffer(buffer); }); }};
error
Event
Fired when an error occurs. Note that the error event doesn’t provide detailed error information for security reasons.
socket.onerror = function (event) { console.error('WebSocket error observed'); // The error event doesn't contain details about what went wrong // Check readyState and wait for close event for more information};
// Errors typically result in connection closuresocket.addEventListener('error', function (event) { console.log('Connection will close due to error');});
Important: The error event is always followed shortly by a close event, which provides more information through the close code and reason.
close
Event
Fired when the connection is closed. The event contains valuable debugging information.
socket.onclose = function (event) { console.log('Connection closed'); console.log('Code:', event.code); console.log('Reason:', event.reason); console.log('Was clean?', event.wasClean);
// Handle different close scenarios if (event.code === 1000) { console.log('Normal closure'); } else if (event.code === 1006) { console.log('Abnormal closure, no close frame'); } else if (event.code >= 4000 && event.code <= 4999) { console.log('Application-specific close code:', event.code); }
// Implement reconnection logic if needed if (!event.wasClean) { setTimeout(() => reconnect(), 5000); }};
Practical Usage Patterns
Reconnection Strategy
Implementing automatic reconnection with exponential backoff:
class ReconnectingWebSocket { constructor(url, protocols = []) { this.url = url; this.protocols = protocols; this.reconnectDelay = 1000; // Start with 1 second this.maxReconnectDelay = 30000; // Max 30 seconds this.reconnectAttempts = 0; this.maxReconnectAttempts = null; // Infinite this.connect(); }
connect() { this.ws = new WebSocket(this.url, this.protocols);
this.ws.onopen = (event) => { console.log('Connected'); this.reconnectDelay = 1000; // Reset delay on successful connection this.reconnectAttempts = 0; this.onopen?.(event); };
this.ws.onmessage = (event) => { this.onmessage?.(event); };
this.ws.onerror = (event) => { console.error('WebSocket error'); this.onerror?.(event); };
this.ws.onclose = (event) => { console.log(`Connection closed: ${event.code} - ${event.reason}`); this.onclose?.(event);
// Attempt reconnection for abnormal closures if (!event.wasClean && this.shouldReconnect()) { setTimeout(() => { console.log( `Reconnecting... (attempt ${this.reconnectAttempts + 1})` ); this.reconnectAttempts++; this.reconnectDelay = Math.min( this.reconnectDelay * 2, this.maxReconnectDelay ); this.connect(); }, this.reconnectDelay); } }; }
shouldReconnect() { return ( this.maxReconnectAttempts === null || this.reconnectAttempts < this.maxReconnectAttempts ); }
send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); } else { console.warn('WebSocket not open. Current state:', this.ws.readyState); } }
close(code = 1000, reason = '') { this.maxReconnectAttempts = 0; // Prevent reconnection this.ws.close(code, reason); }}
// Usageconst socket = new ReconnectingWebSocket('wss://echo.websocket.org');socket.onmessage = (event) => console.log('Received:', event.data);
Heartbeat/Ping-Pong Pattern
Keep connections alive and detect stale connections:
class HeartbeatWebSocket { constructor(url) { this.url = url; this.pingInterval = 30000; // 30 seconds this.pongTimeout = 10000; // 10 seconds to respond this.connect(); }
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { console.log('Connected, starting heartbeat'); this.startHeartbeat(); };
this.ws.onmessage = (event) => { // Reset heartbeat on any message this.startHeartbeat();
// Check for pong response if (event.data === 'pong') { console.log('Received pong'); return; }
// Handle regular messages this.onmessage?.(event); };
this.ws.onclose = () => { console.log('Connection closed'); this.stopHeartbeat(); }; }
startHeartbeat() { this.stopHeartbeat();
this.pingTimer = setTimeout(() => { if (this.ws.readyState === WebSocket.OPEN) { console.log('Sending ping'); this.ws.send('ping');
// Expect pong within timeout this.pongTimer = setTimeout(() => { console.warn('Pong timeout, closing connection'); this.ws.close(4000, 'Ping timeout'); }, this.pongTimeout); } }, this.pingInterval); }
stopHeartbeat() { clearTimeout(this.pingTimer); clearTimeout(this.pongTimer); }
send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); this.startHeartbeat(); // Reset heartbeat on send } }
close() { this.stopHeartbeat(); this.ws.close(); }}
Error Handling Best Practices
Comprehensive error handling and recovery:
class RobustWebSocket { constructor(url, options = {}) { this.url = url; this.options = { maxReconnectAttempts: 5, reconnectInterval: 1000, heartbeatInterval: 30000, messageQueueSize: 100, ...options, };
this.messageQueue = []; this.isReconnecting = false; this.connectionAttempts = 0;
this.connect(); }
connect() { try { this.ws = new WebSocket(this.url); this.setupEventHandlers(); } catch (error) { console.error('Failed to create WebSocket:', error); this.scheduleReconnect(); } }
setupEventHandlers() { this.ws.onopen = (event) => { console.log('Connection established'); this.connectionAttempts = 0; this.isReconnecting = false;
// Flush queued messages while (this.messageQueue.length > 0) { const message = this.messageQueue.shift(); this.send(message); }
this.onopen?.(event); this.startHeartbeat(); };
this.ws.onmessage = (event) => { try { // Attempt to parse JSON messages if ( typeof event.data === 'string' && (event.data.startsWith('{') || event.data.startsWith('[')) ) { const parsed = JSON.parse(event.data); this.onmessage?.({ ...event, parsedData: parsed }); } else { this.onmessage?.(event); } } catch (error) { console.error('Error processing message:', error); this.onerror?.({ type: 'message_processing', error, data: event.data }); } };
this.ws.onerror = (event) => { console.error('WebSocket error occurred'); this.onerror?.(event); };
this.ws.onclose = (event) => { console.log(`Connection closed: ${event.code} - ${event.reason}`); this.stopHeartbeat();
// Determine if we should reconnect if (this.shouldReconnect(event)) { this.scheduleReconnect(); } else { this.onclose?.(event); } }; }
shouldReconnect(closeEvent) { // Don't reconnect for normal closure if (closeEvent.code === 1000) return false;
// Don't reconnect if max attempts reached if (this.connectionAttempts >= this.options.maxReconnectAttempts) { console.error('Max reconnection attempts reached'); return false; }
// Don't reconnect for certain error codes const noReconnectCodes = [ 1002, 1003, 1005, 1006, 1007, 1008, 1009, 1010, 1011, ]; if (noReconnectCodes.includes(closeEvent.code)) { console.error(`Not reconnecting due to close code: ${closeEvent.code}`); return false; }
return true; }
scheduleReconnect() { if (this.isReconnecting) return;
this.isReconnecting = true; this.connectionAttempts++;
const delay = this.options.reconnectInterval * Math.pow(2, this.connectionAttempts - 1); console.log( `Reconnecting in ${delay}ms (attempt ${this.connectionAttempts})` );
setTimeout(() => { this.isReconnecting = false; this.connect(); }, delay); }
send(data) { // Convert objects to JSON const message = typeof data === 'object' ? JSON.stringify(data) : data;
if (this.ws.readyState === WebSocket.OPEN) { try { this.ws.send(message); return true; } catch (error) { console.error('Send failed:', error); this.queueMessage(message); return false; } } else { // Queue message if not connected this.queueMessage(message); return false; } }
queueMessage(message) { if (this.messageQueue.length >= this.options.messageQueueSize) { console.warn('Message queue full, dropping oldest message'); this.messageQueue.shift(); } this.messageQueue.push(message); }
startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send('ping'); } }, this.options.heartbeatInterval); }
stopHeartbeat() { clearInterval(this.heartbeatTimer); }
close(code = 1000, reason = 'Normal closure') { this.stopHeartbeat(); this.messageQueue = []; this.options.maxReconnectAttempts = 0; // Prevent reconnection this.ws.close(code, reason); }
getState() { return { readyState: this.ws?.readyState, isReconnecting: this.isReconnecting, queuedMessages: this.messageQueue.length, connectionAttempts: this.connectionAttempts, }; }}
// Usageconst socket = new RobustWebSocket('wss://echo.websocket.org', { maxReconnectAttempts: 10, reconnectInterval: 2000, heartbeatInterval: 45000,});
socket.onmessage = (event) => { if (event.parsedData) { console.log('Received JSON:', event.parsedData); } else { console.log('Received:', event.data); }};
socket.onerror = (error) => { console.error('Socket error:', error);};
Common Use Cases
Chat Application
class ChatWebSocket { constructor(url, username) { this.username = username; this.ws = new WebSocket(url); this.setupHandlers(); }
setupHandlers() { this.ws.onopen = () => { this.send({ type: 'join', username: this.username, timestamp: Date.now(), }); };
this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleMessage(message); }; }
handleMessage(message) { switch (message.type) { case 'chat': this.onChatMessage?.(message); break; case 'user_joined': this.onUserJoined?.(message); break; case 'user_left': this.onUserLeft?.(message); break; case 'typing': this.onTyping?.(message); break; } }
sendChat(text) { this.send({ type: 'chat', text, username: this.username, timestamp: Date.now(), }); }
sendTyping() { this.send({ type: 'typing', username: this.username, }); }
send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } }}
Live Data Streaming
class DataStreamWebSocket { constructor(url, symbols) { this.url = url; this.symbols = symbols; this.connect(); }
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { // Subscribe to data streams this.ws.send( JSON.stringify({ action: 'subscribe', symbols: this.symbols, }) ); };
this.ws.onmessage = (event) => { const data = JSON.parse(event.data);
// Handle different data types if (data.type === 'price_update') { this.onPriceUpdate?.(data); } else if (data.type === 'volume_update') { this.onVolumeUpdate?.(data); } };
this.ws.onerror = () => { console.error('Stream error, reconnecting...'); setTimeout(() => this.connect(), 5000); }; }
addSymbol(symbol) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send( JSON.stringify({ action: 'subscribe', symbols: [symbol], }) ); this.symbols.push(symbol); } }
removeSymbol(symbol) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send( JSON.stringify({ action: 'unsubscribe', symbols: [symbol], }) ); this.symbols = this.symbols.filter((s) => s !== symbol); } }}
// Usageconst stream = new DataStreamWebSocket('wss://stream.example.com', [ 'AAPL', 'GOOGL',]);stream.onPriceUpdate = (data) => { console.log(`${data.symbol}: $${data.price}`);};
Gaming and Real-time Collaboration
class GameWebSocket { constructor(url, playerId) { this.playerId = playerId; this.ws = new WebSocket(url); this.latency = 0; this.setupHandlers(); }
setupHandlers() { this.ws.onopen = () => { // Join game this.send({ type: 'join', playerId: this.playerId, });
// Start latency monitoring this.measureLatency(); };
this.ws.onmessage = (event) => { const message = JSON.parse(event.data);
// Handle ping response for latency measurement if (message.type === 'pong') { this.latency = Date.now() - message.timestamp; return; }
// Handle game events this.handleGameEvent(message); }; }
handleGameEvent(event) { switch (event.type) { case 'player_move': this.onPlayerMove?.(event); break; case 'game_state': this.onGameState?.(event); break; case 'player_action': this.onPlayerAction?.(event); break; } }
sendMove(x, y) { this.send({ type: 'move', playerId: this.playerId, x, y, timestamp: Date.now(), }); }
sendAction(action, data) { this.send({ type: 'action', playerId: this.playerId, action, data, timestamp: Date.now(), }); }
measureLatency() { setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.send({ type: 'ping', timestamp: Date.now(), }); } }, 5000); }
send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } }
getLatency() { return this.latency; }}
Performance Considerations
Message Size and Frequency
- Keep messages small: Large messages increase latency and memory usage
- Batch when possible: Combine multiple small updates into single messages
- Use binary for large data: More efficient than base64-encoded text
- Implement rate limiting: Prevent overwhelming server or network
Buffer Management
class BufferedWebSocket { constructor(url, options = {}) { this.url = url; this.maxBufferSize = options.maxBufferSize || 1024 * 1024; // 1MB default this.flushInterval = options.flushInterval || 100; // 100ms default this.buffer = []; this.connect(); }
connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => this.startFlushing(); this.ws.onclose = () => this.stopFlushing(); }
send(data) { // Check WebSocket buffer if (this.ws.bufferedAmount > this.maxBufferSize) { console.warn('WebSocket buffer full, dropping message'); return false; }
// Add to internal buffer this.buffer.push(data);
// Flush immediately if buffer is large if (JSON.stringify(this.buffer).length > this.maxBufferSize / 2) { this.flush(); }
return true; }
flush() { if (this.buffer.length === 0) return; if (this.ws.readyState !== WebSocket.OPEN) return;
// Send batched message this.ws.send( JSON.stringify({ type: 'batch', messages: this.buffer, timestamp: Date.now(), }) );
this.buffer = []; }
startFlushing() { this.flushTimer = setInterval(() => this.flush(), this.flushInterval); }
stopFlushing() { clearInterval(this.flushTimer); }}
Connection Pooling
class WebSocketPool { constructor(url, poolSize = 3) { this.url = url; this.poolSize = poolSize; this.connections = []; this.currentIndex = 0;
this.initialize(); }
initialize() { for (let i = 0; i < this.poolSize; i++) { const ws = new WebSocket(this.url); ws.onopen = () => console.log(`Pool connection ${i} ready`); this.connections.push(ws); } }
getConnection() { // Round-robin selection const connection = this.connections[this.currentIndex]; this.currentIndex = (this.currentIndex + 1) % this.poolSize; return connection; }
send(data) { const ws = this.getConnection(); if (ws.readyState === WebSocket.OPEN) { ws.send(data); return true; } return false; }
broadcast(data) { this.connections.forEach((ws) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data); } }); }
close() { this.connections.forEach((ws) => ws.close()); }}
TypeScript Support
Type Definitions
TypeScript includes built-in type definitions for the WebSocket API:
// WebSocket constructorconst socket: WebSocket = new WebSocket(url: string | URL, protocols?: string | string[]);
// WebSocket propertiesconst state: number = socket.readyState;const url: string = socket.url;const protocol: string = socket.protocol;const bufferedAmount: number = socket.bufferedAmount;const binaryType: BinaryType = socket.binaryType; // 'blob' | 'arraybuffer'const extensions: string = socket.extensions;
// WebSocket methodssocket.send(data: string | ArrayBuffer | Blob | ArrayBufferView): void;socket.close(code?: number, reason?: string): void;
// WebSocket eventssocket.onopen = (event: Event) => void;socket.onmessage = (event: MessageEvent) => void;socket.onerror = (event: Event) => void;socket.onclose = (event: CloseEvent) => void;
Typed Message Interfaces
Define custom types for your WebSocket messages:
// Define message typesinterface ChatMessage { type: 'chat'; username: string; text: string; timestamp: number;}
interface UserJoinedMessage { type: 'user_joined'; username: string; timestamp: number;}
interface UserLeftMessage { type: 'user_left'; username: string; timestamp: number;}
type WebSocketMessage = ChatMessage | UserJoinedMessage | UserLeftMessage;
// Type guard functionsfunction isChatMessage(msg: WebSocketMessage): msg is ChatMessage { return msg.type === 'chat';}
function isUserJoinedMessage(msg: WebSocketMessage): msg is UserJoinedMessage { return msg.type === 'user_joined';}
// Usage with type safetysocket.onmessage = (event: MessageEvent) => { const message: WebSocketMessage = JSON.parse(event.data);
if (isChatMessage(message)) { console.log(`${message.username}: ${message.text}`); } else if (isUserJoinedMessage(message)) { console.log(`${message.username} joined the chat`); }};
Typed WebSocket Wrapper Class
Create a fully typed WebSocket wrapper with generics:
interface WebSocketConfig { url: string; protocols?: string | string[]; reconnect?: boolean; reconnectInterval?: number; maxReconnectAttempts?: number;}
interface WebSocketEvents<T> { onOpen?: (event: Event) => void; onMessage?: (data: T) => void; onError?: (event: Event) => void; onClose?: (event: CloseEvent) => void;}
class TypedWebSocket<TSend, TReceive> { private ws: WebSocket | null = null; private config: Required<WebSocketConfig>; private events: WebSocketEvents<TReceive> = {}; private reconnectAttempts = 0;
constructor(config: WebSocketConfig) { this.config = { reconnect: true, reconnectInterval: 1000, maxReconnectAttempts: 5, protocols: undefined, ...config, }; this.connect(); }
private connect(): void { try { this.ws = new WebSocket(this.config.url, this.config.protocols); this.setupEventHandlers(); } catch (error) { console.error('Failed to create WebSocket:', error); this.scheduleReconnect(); } }
private setupEventHandlers(): void { if (!this.ws) return;
this.ws.onopen = (event: Event) => { this.reconnectAttempts = 0; this.events.onOpen?.(event); };
this.ws.onmessage = (event: MessageEvent) => { try { const data: TReceive = JSON.parse(event.data); this.events.onMessage?.(data); } catch (error) { console.error('Failed to parse message:', error); } };
this.ws.onerror = (event: Event) => { this.events.onError?.(event); };
this.ws.onclose = (event: CloseEvent) => { this.events.onClose?.(event); if (this.config.reconnect && !event.wasClean) { this.scheduleReconnect(); } }; }
private scheduleReconnect(): void { if (this.reconnectAttempts >= this.config.maxReconnectAttempts) { console.error('Max reconnection attempts reached'); return; }
this.reconnectAttempts++; setTimeout(() => this.connect(), this.config.reconnectInterval); }
public send(data: TSend): void { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } else { console.warn('WebSocket is not open'); } }
public close(code?: number, reason?: string): void { this.config.reconnect = false; this.ws?.close(code, reason); }
public on<K extends keyof WebSocketEvents<TReceive>>( event: K, handler: WebSocketEvents<TReceive>[K] ): void { this.events[event] = handler; }
public get readyState(): number { return this.ws?.readyState ?? WebSocket.CLOSED; }
public get isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN; }}
// Usage with type safetyinterface ClientMessage { action: 'subscribe' | 'unsubscribe'; channel: string;}
interface ServerMessage { type: 'update' | 'error'; data: any; timestamp: number;}
const typedSocket = new TypedWebSocket<ClientMessage, ServerMessage>({ url: 'wss://api.example.com', reconnect: true, maxReconnectAttempts: 10,});
typedSocket.on('onMessage', (message) => { // message is typed as ServerMessage if (message.type === 'update') { console.log('Update received:', message.data); }});
// Send is type-safetypedSocket.send({ action: 'subscribe', channel: 'news',});
Enum-based State Management
Using TypeScript enums for better state management:
enum WebSocketState { CONNECTING = 0, OPEN = 1, CLOSING = 2, CLOSED = 3,}
enum CloseCode { NORMAL_CLOSURE = 1000, GOING_AWAY = 1001, PROTOCOL_ERROR = 1002, UNSUPPORTED_DATA = 1003, NO_STATUS_RECEIVED = 1005, ABNORMAL_CLOSURE = 1006, INVALID_FRAME_PAYLOAD = 1007, POLICY_VIOLATION = 1008, MESSAGE_TOO_BIG = 1009, MANDATORY_EXTENSION = 1010, INTERNAL_ERROR = 1011, TLS_HANDSHAKE = 1015,}
class WebSocketManager { private ws: WebSocket;
constructor(url: string) { this.ws = new WebSocket(url); }
public getState(): WebSocketState { return this.ws.readyState as WebSocketState; }
public isState(state: WebSocketState): boolean { return this.ws.readyState === state; }
public close( code: CloseCode = CloseCode.NORMAL_CLOSURE, reason?: string ): void { this.ws.close(code, reason); }
public waitForOpen(): Promise<void> { return new Promise((resolve, reject) => { if (this.isState(WebSocketState.OPEN)) { resolve(); return; }
const handleOpen = () => { this.ws.removeEventListener('open', handleOpen); this.ws.removeEventListener('error', handleError); resolve(); };
const handleError = (error: Event) => { this.ws.removeEventListener('open', handleOpen); this.ws.removeEventListener('error', handleError); reject(error); };
this.ws.addEventListener('open', handleOpen); this.ws.addEventListener('error', handleError); }); }}
Async/Await Pattern with TypeScript
Modern async patterns with proper typing:
class AsyncWebSocket { private ws: WebSocket;
constructor(private url: string) { this.ws = new WebSocket(url); }
async connect(): Promise<void> { return new Promise((resolve, reject) => { this.ws.onopen = () => resolve(); this.ws.onerror = (error) => reject(error); }); }
async send<T>(data: T): Promise<void> { if (this.ws.readyState !== WebSocket.OPEN) { await this.connect(); } this.ws.send(JSON.stringify(data)); }
async receive<T>(): Promise<T> { return new Promise((resolve, reject) => { this.ws.onmessage = (event: MessageEvent) => { try { const data: T = JSON.parse(event.data); resolve(data); } catch (error) { reject(error); } }; this.ws.onerror = (error) => reject(error); }); }
async request<TRequest, TResponse>(data: TRequest): Promise<TResponse> { await this.send(data); return this.receive<TResponse>(); }
close(): void { this.ws.close(); }}
// Usage with async/awaitasync function main() { const ws = new AsyncWebSocket('wss://api.example.com');
try { await ws.connect();
const response = await ws.request< { action: string; id: number }, { status: string; result: any } >({ action: 'getData', id: 123, });
console.log('Response:', response); } catch (error) { console.error('WebSocket error:', error); } finally { ws.close(); }}
Working with Binary Data
Sending Binary Data
// ArrayBufferconst buffer = new ArrayBuffer(8);const view = new DataView(buffer);view.setFloat32(0, 3.14159);view.setInt32(4, 42);socket.send(buffer);
// Typed Arraysconst floats = new Float32Array([1.1, 2.2, 3.3]);socket.send(floats.buffer);
// Uint8Array for bytesconst bytes = new Uint8Array([0xff, 0x00, 0xab, 0xcd]);socket.send(bytes);
// Blob (for file-like data)const blob = new Blob(['Binary data'], { type: 'application/octet-stream' });socket.send(blob);
Receiving Binary Data
socket.binaryType = 'arraybuffer'; // Default and recommended
socket.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { // Process binary data const view = new DataView(event.data); const messageType = view.getUint8(0);
switch (messageType) { case 0x01: // Position update const x = view.getFloat32(1); const y = view.getFloat32(5); updatePosition(x, y); break; case 0x02: // Data packet const length = view.getUint32(1); const data = new Uint8Array(event.data, 5, length); processDataPacket(data); break; } }};
Browser Compatibility
Current Browser Support
The WebSocket API has excellent support across all modern browsers:
Browser | Minimum Version | Notes |
---|---|---|
Chrome | 16+ | Full support, best performance |
Firefox | 11+ | Full support |
Safari | 7+ | Full support |
Edge | 12+ | Full support |
Opera | 12.1+ | Full support |
iOS Safari | 6+ | Full support |
Android Browser | 4.4+ | Full support |
Samsung Internet | 4+ | Full support |
Feature Detection
if ('WebSocket' in window) { // WebSocket is supported const socket = new WebSocket('wss://echo.websocket.org');} else { // Fallback for older browsers console.log('WebSocket not supported'); // Consider using a polyfill or alternative transport}
Browser-Specific Considerations
- Mobile browsers: May close connections when app is backgrounded
- Safari: Stricter certificate validation for wss:// connections
- Firefox: Better error messages in developer console
- Chrome: Best DevTools support for WebSocket debugging
Security Considerations
Always Use Secure WebSockets (WSS)
// Bad - unencryptedconst socket = new WebSocket('ws://api.example.com');
// Good - encryptedconst socket = new WebSocket('wss://api.example.com');
Validate Origin
Servers should validate the Origin header to prevent CSWSH attacks.
Input Validation
socket.onmessage = (event) => { try { const data = JSON.parse(event.data);
// Validate message structure if (!data.type || typeof data.type !== 'string') { throw new Error('Invalid message format'); }
// Sanitize user-generated content if (data.html) { data.html = DOMPurify.sanitize(data.html); }
processMessage(data); } catch (error) { console.error('Invalid message received:', error); }};
Authentication
// Option 1: Token in URL (visible in logs)const socket = new WebSocket('wss://api.example.com/ws?token=' + authToken);
// Option 2: Send auth message after connectionconst socket = new WebSocket('wss://api.example.com/ws');socket.onopen = () => { socket.send( JSON.stringify({ type: 'auth', token: authToken, }) );};
// Option 3: Use cookies (must be set with Secure and SameSite flags)// Cookies are automatically sent with the WebSocket handshake
Rate Limiting
class RateLimitedWebSocket { constructor(url, maxMessagesPerSecond = 10) { this.ws = new WebSocket(url); this.maxRate = maxMessagesPerSecond; this.messageTimestamps = []; }
send(data) { const now = Date.now();
// Remove timestamps older than 1 second this.messageTimestamps = this.messageTimestamps.filter( (t) => now - t < 1000 );
// Check rate limit if (this.messageTimestamps.length >= this.maxRate) { console.warn('Rate limit exceeded'); return false; }
// Send message and record timestamp this.messageTimestamps.push(now); this.ws.send(data); return true; }}
Common Gotchas
bufferedAmount
and Backpressure
The bufferedAmount
property shows data queued but not yet sent. Monitor this
to implement backpressure:
function safeSend(socket, data) { // Prevent memory issues from unbounded buffering const MAX_BUFFER = 10 * 1024 * 1024; // 10MB
if (socket.bufferedAmount > MAX_BUFFER) { console.error('Buffer full, message dropped'); return false; }
socket.send(data); return true;}
Message Size Limits
Different browsers and servers have different limits:
- Chrome: ~100MB per message
- Firefox: ~2GB per message
- Safari: ~100MB per message
- Most servers: Configurable, often 64KB-10MB default
Always handle large data appropriately:
function sendLargeData(socket, data) { const CHUNK_SIZE = 64 * 1024; // 64KB chunks const json = JSON.stringify(data);
if (json.length > CHUNK_SIZE) { // Split into chunks for (let i = 0; i < json.length; i += CHUNK_SIZE) { socket.send( JSON.stringify({ type: 'chunk', id: Date.now(), index: Math.floor(i / CHUNK_SIZE), total: Math.ceil(json.length / CHUNK_SIZE), data: json.substr(i, CHUNK_SIZE), }) ); } } else { socket.send(json); }}
Close Codes and Their Meanings
Not all close codes are equal. Some indicate errors, others normal operation:
socket.onclose = (event) => { const errorCodes = [ 1002, 1003, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1015, ];
if (errorCodes.includes(event.code)) { console.error(`Connection error: ${event.code} - ${event.reason}`); // Implement error recovery } else if (event.code === 1000 || event.code === 1001) { console.log('Normal connection closure'); }};
onerror
Limitations
The error event provides no details about what went wrong for security reasons:
socket.onerror = (event) => { // event doesn't contain error details // Can't determine what went wrong from this event alone console.error('WebSocket error occurred');
// Check other indicators console.log('ReadyState:', socket.readyState); console.log('URL:', socket.url);
// Wait for close event for more information};
lastEventId
Irrelevance
The MessageEvent.lastEventId
property is inherited from Server-Sent Events and
is always empty for WebSockets:
socket.onmessage = (event) => { console.log(event.lastEventId); // Always empty string "" // Don't rely on this - implement your own message IDs if needed};
binaryType
Configuration
Must be set before receiving binary data:
// Set BEFORE receiving any binary messagessocket.binaryType = 'arraybuffer'; // or 'blob'
socket.onopen = () => { // Safe to receive binary data now};
// Changing binaryType only affects future messagessocket.binaryType = 'blob';// Previously received ArrayBuffers don't change to Blobs
Related Interfaces
CloseEvent
The CloseEvent provides information about why a connection was closed:
socket.onclose = (event) => { // CloseEvent properties console.log('Code:', event.code); // Numeric close code console.log('Reason:', event.reason); // Human-readable reason console.log('Clean:', event.wasClean); // Was it a clean close?
// Inherited Event properties console.log('Type:', event.type); // "close" console.log('Target:', event.target); // The WebSocket object};
MessageEvent
The MessageEvent delivers data from the server:
socket.onmessage = (event) => { // MessageEvent properties console.log('Data:', event.data); // The message payload console.log('Origin:', event.origin); // Origin of the message console.log('LastEventId:', event.lastEventId); // Always "" for WebSocket console.log('Source:', event.source); // null for WebSocket console.log('Ports:', event.ports); // [] for WebSocket
// Inherited Event properties console.log('Type:', event.type); // "message" console.log('Target:', event.target); // The WebSocket object};
WebSocket Close Codes
Standard Codes (1000-1015)
Code | Name | Description | Action Required |
---|---|---|---|
1000 | Normal Closure | Standard closing of connection | None |
1001 | Going Away | Server going down or browser navigating away | Reconnect to different endpoint |
1002 | Protocol Error | Protocol error detected | Fix protocol implementation |
1003 | Unsupported Data | Received data type cannot be accepted | Check data format |
1005 | No Status Received | No status code in close frame | Treat as abnormal closure |
1006 | Abnormal Closure | Connection lost without close frame | Check network, implement reconnection |
1007 | Invalid Frame Payload | Message data was inconsistent | Validate message encoding |
1008 | Policy Violation | Generic policy violation | Review server requirements |
1009 | Message Too Big | Message exceeds size limits | Reduce message size |
1010 | Mandatory Extension | Required extension not supported | Negotiate supported extensions |
1011 | Internal Error | Server encountered unexpected condition | Retry with backoff |
1015 | TLS Handshake | TLS/SSL handshake failure | Check certificates |
Application Codes (4000-4999)
Reserved for application-specific use:
// Define your own application codesconst APP_CLOSE_CODES = { IDLE_TIMEOUT: 4000, USER_LOGOUT: 4001, DUPLICATE_CONNECTION: 4002, RATE_LIMIT_EXCEEDED: 4003, AUTHENTICATION_FAILED: 4004, SUBSCRIPTION_EXPIRED: 4005,};
// Use them when closingsocket.close(APP_CLOSE_CODES.IDLE_TIMEOUT, 'Idle for too long');
IoT and Mobile Considerations
Battery Optimization
class MobileWebSocket { constructor(url) { this.url = url; this.isBackground = false;
// Listen for visibility changes document.addEventListener('visibilitychange', () => { if (document.hidden) { this.onBackground(); } else { this.onForeground(); } });
this.connect(); }
onBackground() { this.isBackground = true; // Reduce heartbeat frequency this.heartbeatInterval = 60000; // 1 minute
// Or close connection to save battery if (this.aggressive) { this.ws.close(1000, 'App backgrounded'); } }
onForeground() { this.isBackground = false; // Restore normal heartbeat this.heartbeatInterval = 30000; // 30 seconds
// Reconnect if needed if (this.ws.readyState !== WebSocket.OPEN) { this.connect(); } }
connect() { this.ws = new WebSocket(this.url); // ... setup handlers }}
Live Streaming Patterns
Adaptive Bitrate Streaming
class AdaptiveWebSocket { constructor(url) { this.ws = new WebSocket(url); this.measureBandwidth(); }
measureBandwidth() { let lastTime = Date.now(); let bytesReceived = 0;
this.ws.onmessage = (event) => { const now = Date.now(); const data = event.data;
// Measure throughput if (data instanceof ArrayBuffer) { bytesReceived += data.byteLength; } else { bytesReceived += data.length * 2; // Approximate for UTF-16 }
const elapsed = now - lastTime; if (elapsed > 1000) { // Every second const throughput = ((bytesReceived * 8) / elapsed) * 1000; // bits per second this.adjustQuality(throughput);
// Reset counters bytesReceived = 0; lastTime = now; }
// Handle message normally this.ondata?.(event); }; }
adjustQuality(bitsPerSecond) { let quality; if (bitsPerSecond > 5000000) quality = 'high'; // > 5 Mbps else if (bitsPerSecond > 1000000) quality = 'medium'; // > 1 Mbps else quality = 'low';
if (this.currentQuality !== quality) { this.currentQuality = quality; this.ws.send( JSON.stringify({ type: 'quality', quality: quality, }) ); } }}
Further Reading
Specifications and Standards
- WHATWG HTML Living Standard - WebSockets - The official WebSocket API specification
- RFC 6455 - The WebSocket Protocol - IETF protocol specification
- RFC 7692 - Compression Extensions - WebSocket compression
- RFC 8441 - Bootstrapping WebSockets with HTTP/2 - HTTP/2 support
WebSocket.org Resources
- WebSocket Protocol Guide - Deep dive into the protocol
- Building WebSocket Applications - Practical implementation guide
- WebSockets at Scale - Scaling strategies and patterns
- Future of WebSockets - HTTP/3 and upcoming features
Developer Resources
- MDN WebSocket Documentation - Mozilla’s comprehensive guide
- Chrome DevTools WebSocket Debugging - Debug WebSocket connections
- Can I Use WebSocket - Browser compatibility data
Testing Tools
- WebSocket Echo Server - Test server for development
- Online WebSocket Test - Browser-based testing tool