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)