WebSockets & Real-Time Apps

Native WebSocket API in the Browser

20 min Lesson 3 of 35

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.