The WebSocket Constructor
The browser's native WebSocket API provides a simple interface for establishing WebSocket connections. You create a connection using the WebSocket constructor:
// Basic WebSocket connection
const socket = new WebSocket('wss://example.com/socket');
// WebSocket with subprotocol
const socketWithProtocol = new WebSocket(
'wss://example.com/socket',
'chat-v1'
);
// WebSocket with multiple subprotocols (server picks one)
const socketMultiProtocol = new WebSocket(
'wss://example.com/socket',
['chat-v2', 'chat-v1']
);
Constructor Parameters:
- url (required): The WebSocket URL (ws:// or wss://)
- protocols (optional): String or array of subprotocol names
Subprotocols: Subprotocols allow you to define custom application-level protocols on top of WebSocket. For example, 'mqtt', 'stomp', or custom protocols like 'chat-v1'. The server must support the requested subprotocol for the connection to succeed.
WebSocket Events
The WebSocket object emits four main events during its lifecycle. You can listen to these events using event listeners or event handler properties:
1. onopen Event
Fired when the WebSocket connection is successfully established:
const socket = new WebSocket('wss://example.com/socket');
// Using event handler property
socket.onopen = (event) => {
console.log('Connection opened');
console.log('Protocol:', socket.protocol);
console.log('Extensions:', socket.extensions);
// Now safe to send messages
socket.send('Hello Server!');
};
// Using addEventListener (allows multiple listeners)
socket.addEventListener('open', (event) => {
console.log('WebSocket is now open');
});
Best Practice: Always wait for the 'open' event before sending messages. Attempting to send before the connection is established will throw an error.
2. onmessage Event
Fired when data is received from the server:
socket.onmessage = (event) => {
console.log('Received:', event.data);
// Handle different data types
if (typeof event.data === 'string') {
console.log('Text message:', event.data);
// Parse JSON if applicable
try {
const data = JSON.parse(event.data);
console.log('Parsed JSON:', data);
} catch (e) {
console.log('Not JSON data');
}
} else if (event.data instanceof Blob) {
console.log('Binary message (Blob):', event.data);
// Read blob data
event.data.text().then(text => {
console.log('Blob content:', text);
});
} else if (event.data instanceof ArrayBuffer) {
console.log('Binary message (ArrayBuffer):', event.data);
// Process binary data
const view = new Uint8Array(event.data);
console.log('First byte:', view[0]);
}
};
MessageEvent Properties:
- data: The message data (string, Blob, or ArrayBuffer)
- origin: The origin of the message sender
- lastEventId: Last event ID (for SSE, not typically used in WebSocket)
3. onerror Event
Fired when an error occurs (connection failure, network error, etc.):
socket.onerror = (event) => {
console.error('WebSocket error:', event);
// Note: The Event object doesn't contain detailed error info
// for security reasons. Check the console for more details.
console.log('Error type:', event.type);
console.log('ReadyState:', socket.readyState);
};
Limited Error Information: For security reasons, the error event doesn't provide detailed information about what went wrong. You'll need to use browser developer tools or server-side logging to diagnose connection issues.
4. onclose Event
Fired when the connection is closed (by either party or due to an error):
socket.onclose = (event) => {
console.log('Connection closed');
console.log('Clean close:', event.wasClean);
console.log('Close code:', event.code);
console.log('Close reason:', event.reason);
// Handle different close codes
switch (event.code) {
case 1000:
console.log('Normal closure');
break;
case 1001:
console.log('Going away (page navigation or server shutdown)');
break;
case 1006:
console.log('Abnormal closure (no close frame received)');
// This typically means network error or crashed server
break;
default:
console.log('Closed with code:', event.code);
}
// Implement reconnection logic
setTimeout(() => {
console.log('Attempting to reconnect...');
reconnect();
}, 5000);
};
CloseEvent Properties:
- wasClean: Boolean indicating if connection closed cleanly
- code: Numeric close status code
- reason: String explaining why the connection closed
The send() Method
The send() method transmits data to the server. It can send text, Blob, ArrayBuffer, or ArrayBufferView data:
Sending Text Data
// Send plain text
socket.send('Hello Server!');
// Send JSON data (stringify first)
const data = { type: 'message', content: 'Hello!', userId: 123 };
socket.send(JSON.stringify(data));
Sending Binary Data
// Send ArrayBuffer
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view[0] = 255;
view[1] = 128;
socket.send(buffer);
// Send Typed Array (ArrayBufferView)
const uint8Array = new Uint8Array([1, 2, 3, 4, 5]);
socket.send(uint8Array);
// Send Blob
const blob = new Blob(['Hello World'], { type: 'text/plain' });
socket.send(blob);
Binary Data Format: By default, binary data is received as Blob. You can change this using the binaryType property:
socket.binaryType = 'arraybuffer'; // or 'blob' (default)
Checking Buffer Status
// Check buffered amount before sending large data
function sendLargeData(data) {
if (socket.bufferedAmount === 0) {
socket.send(data);
} else {
console.log('Buffer not empty, waiting...');
console.log('Buffered bytes:', socket.bufferedAmount);
// Wait and retry
setTimeout(() => sendLargeData(data), 100);
}
}
bufferedAmount Property: Returns the number of bytes queued but not yet transmitted. Useful for flow control when sending large amounts of data.
The readyState Property
The readyState property indicates the connection's current state:
// WebSocket.CONNECTING === 0
// WebSocket.OPEN === 1
// WebSocket.CLOSING === 2
// WebSocket.CLOSED === 3
console.log('Current state:', socket.readyState);
// Check state before sending
if (socket.readyState === WebSocket.OPEN) {
socket.send('Message');
} else if (socket.readyState === WebSocket.CONNECTING) {
console.log('Still connecting, wait for open event');
} else {
console.log('Connection is closing or closed');
}
// Helper function to check if socket is usable
function isSocketOpen(socket) {
return socket && socket.readyState === WebSocket.OPEN;
}
State Constants: Use the named constants (WebSocket.OPEN) instead of magic numbers (1) for better code readability and maintainability.
Receiving Binary Data
Configure how binary data is received using the binaryType property:
// Set binary type to ArrayBuffer (more efficient for processing)
socket.binaryType = 'arraybuffer';
socket.onmessage = (event) => {
if (typeof event.data === 'string') {
console.log('Text:', event.data);
} else if (event.data instanceof ArrayBuffer) {
const bytes = new Uint8Array(event.data);
console.log('Received binary data:', bytes.length, 'bytes');
// Process binary data
for (let i = 0; i < bytes.length; i++) {
console.log(`Byte ${i}:`, bytes[i]);
}
}
};
// Alternatively, use Blob (default)
socket.binaryType = 'blob';
socket.onmessage = async (event) => {
if (event.data instanceof Blob) {
// Read blob as text
const text = await event.data.text();
console.log('Blob as text:', text);
// Or read as ArrayBuffer
const buffer = await event.data.arrayBuffer();
console.log('Blob as buffer:', buffer);
}
};
Choosing binaryType:
- 'arraybuffer': Better for direct binary processing, lower overhead
- 'blob': Better for large files, can be streamed, works well with File API
Closing Connections Gracefully
Always close WebSocket connections properly using the close() method:
// Simple close
socket.close();
// Close with status code
socket.close(1000); // Normal closure
// Close with status code and reason
socket.close(1000, 'User logged out');
// Custom application-level close codes (3000-4999)
socket.close(3000, 'Session expired');
socket.close(4001, 'Invalid authentication token');
Valid Close Codes:
- 1000-1999: Reserved for WebSocket protocol (use 1000 for normal closure)
- 3000-3999: Reserved for libraries and frameworks
- 4000-4999: Available for application use
Avoid Abrupt Closure: Don't just set socket to null or navigate away without calling close(). This creates "half-open" connections on the server that waste resources until they timeout.
Complete WebSocket Example
Here's a complete example implementing all WebSocket features with error handling and reconnection:
class WebSocketClient {
constructor(url) {
this.url = url;
this.socket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.connect();
}
connect() {
try {
this.socket = new WebSocket(this.url);
this.socket.binaryType = 'arraybuffer';
this.socket.onopen = (event) => {
console.log('Connected to WebSocket server');
this.reconnectAttempts = 0; // Reset on successful connection
this.onOpen(event);
};
this.socket.onmessage = (event) => {
this.onMessage(event);
};
this.socket.onerror = (event) => {
console.error('WebSocket error:', event);
this.onError(event);
};
this.socket.onclose = (event) => {
console.log('Connection closed:', event.code, event.reason);
this.onClose(event);
// Attempt reconnection
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnect();
} else {
console.error('Max reconnection attempts reached');
}
};
} catch (error) {
console.error('Failed to create WebSocket:', error);
}
}
reconnect() {
this.reconnectAttempts++;
const delay = this.reconnectDelay * this.reconnectAttempts;
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.connect();
}, delay);
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
if (typeof data === 'object') {
this.socket.send(JSON.stringify(data));
} else {
this.socket.send(data);
}
} else {
console.error('Socket is not open. ReadyState:', this.socket?.readyState);
}
}
close(code = 1000, reason = 'Client closed connection') {
if (this.socket) {
this.socket.close(code, reason);
}
}
// Override these methods in your implementation
onOpen(event) {
// Handle connection opened
}
onMessage(event) {
// Handle incoming messages
console.log('Received:', event.data);
}
onError(event) {
// Handle errors
}
onClose(event) {
// Handle connection closed
}
}
// Usage
const client = new WebSocketClient('wss://example.com/socket');
// Override handlers
client.onMessage = (event) => {
const data = JSON.parse(event.data);
console.log('Message received:', data);
};
// Send messages
setTimeout(() => {
client.send({ type: 'chat', message: 'Hello!' });
}, 1000);
Exercise: Build a simple chat client using the native WebSocket API:
- Connect to a WebSocket echo server (wss://echo.websocket.org/)
- Create an input field and send button
- Display sent and received messages in a list
- Show connection status (connecting, open, closed)
- Implement automatic reconnection on disconnect
WebSocket URL Parameters
Pass authentication tokens or other parameters via URL query strings:
// Add query parameters
const token = 'abc123xyz';
const userId = 42;
const url = `wss://example.com/socket?token=${token}&userId=${userId}`;
const socket = new WebSocket(url);
// Server can parse URL parameters to authenticate/identify client
Security Consideration: URL parameters appear in server logs and browser history. For sensitive data like authentication tokens, consider sending them in the first message after connection instead, or use custom headers during the handshake (requires server support).
Summary
The native WebSocket API provides a straightforward interface for real-time bidirectional communication. Key concepts include the WebSocket constructor, four lifecycle events (open, message, error, close), the send() method for transmitting data, readyState for monitoring connection state, and the close() method for graceful disconnection. Understanding binary data handling and implementing proper reconnection logic are essential for production applications. In the next lesson, we'll explore building a WebSocket server with Node.js.