WebSockets & Real-Time Apps

Events and Event Handling

17 min Lesson 8 of 35

Events and Event Handling

Events are the core communication mechanism in Socket.io. Understanding how to properly create, emit, listen for, and handle events is essential for building robust real-time applications.

Built-in Events

Socket.io provides several built-in events that you should never emit manually:

// CLIENT-SIDE BUILT-IN EVENTS socket.on('connect', () => { // Fired when successfully connected }); socket.on('disconnect', (reason) => { // Fired when disconnected }); socket.on('connect_error', (error) => { // Fired when connection fails }); // SERVER-SIDE BUILT-IN EVENTS io.on('connection', (socket) => { // Fired when client connects socket.on('disconnect', (reason) => { // Fired when client disconnects }); socket.on('disconnecting', () => { // Fired before disconnect (rooms still accessible) }); });
Warning: Never manually emit built-in events like connect, disconnect, connection, etc. These are reserved by Socket.io and emitting them will cause unexpected behavior.

Custom Events

You can create custom events with any name (except reserved names):

// SERVER io.on('connection', (socket) => { // Emit custom event to client socket.emit('welcomeMessage', 'Hello new user!'); socket.emit('serverTime', new Date().toISOString()); socket.emit('userData', { id: 123, name: 'John' }); // Listen for custom events from client socket.on('chatMessage', (message) => { console.log('Received:', message); }); socket.on('userAction', (action, data) => { console.log(`User performed ${action}:`, data); }); }); // CLIENT socket.emit('chatMessage', 'Hello everyone!'); socket.emit('userAction', 'click', { button: 'submit' }); socket.on('welcomeMessage', (msg) => { console.log(msg); }); socket.on('userData', (user) => { console.log('User data:', user); });

Event Naming Conventions

Follow these best practices for naming events:

// GOOD - Clear and descriptive socket.emit('chatMessage', data); socket.emit('userJoined', username); socket.emit('gameStateUpdate', state); socket.emit('notificationReceived', notification); // GOOD - Namespaced events for organization socket.emit('chat:message', data); socket.emit('chat:typing', isTyping); socket.emit('game:start', gameId); socket.emit('game:move', moveData); // AVOID - Too generic socket.emit('data', data); socket.emit('update', update); socket.emit('event', event); // AVOID - Reserved names socket.emit('connect', data); // Reserved! socket.emit('disconnect', data); // Reserved!
Tip: Use a colon or underscore to namespace related events (e.g., chat:message, chat:typing) to keep your code organized, especially in large applications.

Event Acknowledgements (Callbacks)

You can request acknowledgment when emitting events, similar to HTTP request-response:

// CLIENT - Send with callback socket.emit('createUser', { name: 'John', email: 'john@example.com' }, (response) => { if (response.success) { console.log('User created with ID:', response.userId); } else { console.error('Error:', response.error); } }); // SERVER - Call the callback io.on('connection', (socket) => { socket.on('createUser', (userData, callback) => { // Validate data if (!userData.name || !userData.email) { callback({ success: false, error: 'Missing required fields' }); return; } // Create user const userId = Math.random().toString(36).substr(2, 9); // Send response via callback callback({ success: true, userId: userId }); }); });

Acknowledgements can also be used from server to client:

// SERVER - Emit with callback socket.emit('confirmAction', 'Delete all messages?', (confirmed) => { if (confirmed) { console.log('User confirmed deletion'); // Proceed with deletion } else { console.log('User cancelled'); } }); // CLIENT - Respond to callback socket.on('confirmAction', (message, callback) => { const confirmed = confirm(message); callback(confirmed); });

Timeout for Acknowledgements

Set a timeout to handle cases where the other side doesn't respond:

// Socket.io 4.0+ socket.timeout(5000).emit('requestData', (err, response) => { if (err) { // Timeout occurred console.error('Request timed out'); } else { console.log('Response received:', response); } }); // Manual timeout handling const timeoutId = setTimeout(() => { console.error('Manual timeout'); }, 5000); socket.emit('requestData', (response) => { clearTimeout(timeoutId); console.log('Response:', response); });

Volatile Events

Volatile events are not buffered and will be lost if the client is not ready to receive them. Useful for real-time data that becomes stale quickly:

// SERVER - Send volatile event socket.volatile.emit('cursorPosition', { x: 100, y: 200 }); socket.volatile.emit('gameTickUpdate', gameState); // If client is not ready, these events are dropped // Good for high-frequency updates where occasional loss is acceptable
Note: Use volatile events for high-frequency updates like cursor positions, game ticks, or sensor data where missing a few updates is acceptable. For critical events like chat messages or transactions, use regular emit.

Error Events

Handle errors gracefully with dedicated error events:

// SERVER - Emit errors io.on('connection', (socket) => { socket.on('performAction', (data) => { try { // Process action if (!data.userId) { throw new Error('User ID required'); } // Success socket.emit('actionSuccess', { message: 'Action completed' }); } catch (error) { // Send error to client socket.emit('actionError', { message: error.message, code: 'ACTION_FAILED' }); } }); // Handle socket-level errors socket.on('error', (error) => { console.error('Socket error:', error); }); }); // CLIENT - Listen for errors socket.on('actionError', (error) => { alert(`Error: ${error.message}`); console.error('Error code:', error.code); }); socket.on('error', (error) => { console.error('Connection error:', error); });

Catch-All Listeners (onAny)

Listen for all events with a catch-all listener, useful for logging and debugging:

// SERVER - Listen to all incoming events io.on('connection', (socket) => { socket.onAny((eventName, ...args) => { console.log(`Event received: ${eventName}`); console.log('Arguments:', args); }); // Listen to all outgoing events socket.onAnyOutgoing((eventName, ...args) => { console.log(`Event sent: ${eventName}`); console.log('Arguments:', args); }); }); // CLIENT - Listen to all events from server socket.onAny((eventName, ...args) => { console.log(`Received event: ${eventName}`, args); }); // Remove catch-all listener socket.offAny(listenerFunction);
Tip: Use onAny() during development for debugging, but be careful in production as it can impact performance if you log every event.

Removing Event Listeners

Clean up listeners to prevent memory leaks:

// Remove specific listener const messageHandler = (msg) => { console.log('Message:', msg); }; socket.on('chatMessage', messageHandler); socket.off('chatMessage', messageHandler); // Remove // Remove all listeners for an event socket.removeAllListeners('chatMessage'); // Remove all listeners for all events socket.removeAllListeners(); // One-time listener (auto-removes after first trigger) socket.once('welcome', (msg) => { console.log('Welcome message received once:', msg); });

Event Data Best Practices

// GOOD - Send structured data socket.emit('chatMessage', { userId: '12345', username: 'John', message: 'Hello!', timestamp: Date.now(), room: 'general' }); // GOOD - Use consistent data shapes socket.emit('notification', { type: 'info', // info, warning, error title: 'New Message', message: 'You have a new message', timestamp: Date.now() }); // AVOID - Sending too much data socket.emit('userUpdate', entireUserDatabase); // Too large! // AVOID - Inconsistent data structures socket.emit('message', 'just a string'); // Sometimes string socket.emit('message', { text: 'object' }); // Sometimes object

Complete Event Handling Example

// SERVER const express = require('express'); const { createServer } = require('http'); const { Server } = require('socket.io'); const app = express(); const httpServer = createServer(app); const io = new Server(httpServer); io.on('connection', (socket) => { console.log('User connected:', socket.id); // Send welcome with acknowledgement socket.emit('welcome', 'Welcome to the chat!', (response) => { console.log('Client acknowledged:', response); }); // Handle chat messages socket.on('chat:message', (data, callback) => { // Validate if (!data.message || data.message.trim() === '') { callback({ success: false, error: 'Empty message' }); return; } // Broadcast to all io.emit('chat:message', { id: socket.id, message: data.message, timestamp: new Date() }); callback({ success: true }); }); // Handle typing indicator (volatile) socket.on('chat:typing', (isTyping) => { socket.volatile.broadcast.emit('chat:typing', { userId: socket.id, isTyping: isTyping }); }); // Catch-all for debugging socket.onAny((eventName, ...args) => { console.log(`📥 ${eventName}:`, args); }); // Handle errors socket.on('error', (error) => { console.error('Socket error:', error); }); socket.on('disconnect', () => { console.log('User disconnected:', socket.id); }); }); httpServer.listen(3000);
Exercise: Create a Socket.io application with event handling:
  • Implement custom events for chat messages, user typing, and user status
  • Use acknowledgements to confirm message delivery
  • Add error events for validation failures
  • Implement volatile events for typing indicators
  • Use a catch-all listener to log all events during development
  • Create a one-time event listener for the welcome message
  • Follow naming conventions with namespaced events (chat:message, user:typing)