This guide provides best practices, patterns, and detailed information for using node-osc effectively.
The Server class extends EventEmitter and emits several events for different scenarios.
Emitted when the server starts listening for messages.
server.on('listening', () => {
console.log('Server is ready to receive messages');
});Emitted when an OSC message is received.
Parameters:
msg(Array): The message as an array where the first element is the address and subsequent elements are argumentsrinfo(Object): Remote address informationaddress(string): The sender's IP addressport(number): The sender's port number
server.on('message', (msg, rinfo) => {
const [address, ...args] = msg;
console.log(`Received ${address} from ${rinfo.address}:${rinfo.port}`);
console.log('Arguments:', args);
});Emitted when an OSC bundle is received.
Parameters:
bundle(Object): The bundle objecttimetag(number): The bundle's timetagelements(Array): Array of messages or nested bundles
rinfo(Object): Remote address information
server.on('bundle', (bundle, rinfo) => {
console.log(`Received bundle with timetag ${bundle.timetag}`);
bundle.elements.forEach((element) => {
console.log('Element:', element);
});
});The server also emits events for each message address received. This allows you to listen for specific OSC addresses without filtering in your code.
// Listen specifically for messages to /note
server.on('/note', (msg, rinfo) => {
const [address, pitch, velocity] = msg;
console.log(`Note: ${pitch}, Velocity: ${velocity}`);
});
// Listen for /oscillator/frequency
server.on('/oscillator/frequency', (msg) => {
const [address, freq] = msg;
console.log(`Frequency set to ${freq} Hz`);
});Emitted when there's an error decoding an incoming message or a socket error.
Parameters:
error(Error): The error objectrinfo(Object): Remote address information (for decode errors)
server.on('error', (error, rinfo) => {
if (rinfo) {
console.error(`Error from ${rinfo.address}:${rinfo.port}: ${error.message}`);
} else {
console.error('Socket error:', error.message);
}
});Emitted when a socket error occurs.
client.on('error', (error) => {
console.error('Client error:', error.message);
});Proper error handling is essential for robust OSC applications.
If you try to send a message after closing the client, a ReferenceError will be thrown:
const client = new Client('127.0.0.1', 3333);
await client.close();
try {
await client.send('/test', 123);
} catch (err) {
console.error(err.message); // "Cannot send message on closed socket."
console.error(err.code); // "ERR_SOCKET_DGRAM_NOT_RUNNING"
}Prevention: Always ensure the client is open before sending:
const client = new Client('127.0.0.1', 3333);
try {
await client.send('/test', 123);
} finally {
await client.close(); // Close after sending
}Passing an invalid message format will throw a TypeError:
try {
await client.send(12345); // Not a valid message format
} catch (err) {
console.error(err.message); // "That Message Just Doesn't Seem Right"
}When the server receives malformed OSC data, it emits an 'error' event rather than throwing:
server.on('error', (err, rinfo) => {
console.error(`Decode error from ${rinfo.address}: ${err.message}`);
});client.send('/test', 123, (err) => {
if (err) {
console.error('Send failed:', err);
return;
}
console.log('Message sent successfully');
});try {
await client.send('/test', 123);
console.log('Message sent successfully');
} catch (err) {
console.error('Send failed:', err);
}Use try/finally to ensure resources are cleaned up even if errors occur:
const client = new Client('127.0.0.1', 3333);
try {
await client.send('/test', 123);
await client.send('/test', 456);
} catch (err) {
console.error('Error sending:', err);
} finally {
await client.close(); // Always executes
}OSC supports several data types. node-osc automatically detects types for common JavaScript values:
| JavaScript Type | OSC Type | Description |
|---|---|---|
| Integer number | integer |
Whole numbers (e.g., 42, -10, 0) |
| Float number | float |
Decimal numbers (e.g., 3.14, -0.5) |
| String | string |
Text values (e.g., "hello") |
| Boolean | boolean |
true or false |
| Buffer | blob |
Binary data |
| MIDI object/Buffer | midi |
MIDI messages (4 bytes: port, status, data1, data2) |
const msg = new Message('/test');
msg.append(42); // → integer
msg.append(3.14); // → float
msg.append('hello'); // → string
msg.append(true); // → booleanFor advanced use cases, you can explicitly specify types:
const msg = new Message('/test');
// Force a whole number to be sent as float
msg.append({ type: 'float', value: 42 });
// Use shorthand type notation
msg.append({ type: 'f', value: 42 }); // 'f' = float
msg.append({ type: 'i', value: 3.14 }); // 'i' = integer (truncates)
msg.append({ type: 's', value: 'text' }); // 's' = string
msg.append({ type: 'b', value: Buffer.from('data') }); // 'b' = blob
msg.append({ type: 'm', value: { port: 0, status: 144, data1: 60, data2: 127 } }); // 'm' = MIDI'i'or'integer'- 32-bit integer'f'or'float'- 32-bit float's'or'string'- OSC string'b'or'blob'- Binary blob'm'or'midi'- MIDI message (4 bytes)'boolean'- Boolean value (true/false)'T'- True'F'- False
Prefer async/await over callbacks for more readable code:
// ✅ Good - Clean and readable
async function sendMessages() {
const client = new Client('127.0.0.1', 3333);
try {
await client.send('/test', 1);
await client.send('/test', 2);
await client.send('/test', 3);
} finally {
await client.close();
}
}
// ❌ Less ideal - Callback pyramid
function sendMessages() {
const client = new Client('127.0.0.1', 3333);
client.send('/test', 1, (err) => {
if (err) return console.error(err);
client.send('/test', 2, (err) => {
if (err) return console.error(err);
client.send('/test', 3, (err) => {
if (err) return console.error(err);
client.close();
});
});
});
}Always close clients and servers when done to prevent resource leaks:
const client = new Client('127.0.0.1', 3333);
try {
await client.send('/test', 123);
} finally {
await client.close(); // Always close
}For better code organization, use address-specific event listeners:
// ✅ Good - Clear and organized
server.on('/note', (msg) => {
handleNote(msg);
});
server.on('/control', (msg) => {
handleControl(msg);
});
// ❌ Less ideal - Manual routing
server.on('message', (msg) => {
const [address] = msg;
if (address === '/note') handleNote(msg);
else if (address === '/control') handleControl(msg);
});Always implement error handling for both clients and servers:
// Client
try {
await client.send('/test', 123);
} catch (err) {
console.error('Failed to send:', err.message);
}
// Server
server.on('error', (err, rinfo) => {
console.error(`Server error from ${rinfo?.address}:`, err.message);
});When sending multiple related messages, use bundles for atomic operations:
// ✅ Good - Atomic operation
const bundle = new Bundle(
['/synth/freq', 440],
['/synth/amp', 0.5],
['/synth/gate', 1]
);
await client.send(bundle);
// ❌ Less ideal - Separate messages (not atomic)
await client.send('/synth/freq', 440);
await client.send('/synth/amp', 0.5);
await client.send('/synth/gate', 1);If you need to receive messages from other machines:
// Listen on all interfaces (accessible from network)
const server = new Server(3333, '0.0.0.0');
// Only localhost (default, more secure)
const server = new Server(3333, '127.0.0.1');Follow OSC naming conventions with hierarchical addresses:
// ✅ Good - Hierarchical and descriptive
await client.send('/synth/oscillator/1/frequency', 440);
await client.send('/mixer/channel/3/volume', 0.8);
// ❌ Less ideal - Flat and unclear
await client.send('/freq1', 440);
await client.send('/vol3', 0.8);Validate data before sending to avoid runtime errors:
function sendNote(pitch, velocity) {
if (typeof pitch !== 'number' || pitch < 0 || pitch > 127) {
throw new Error('Invalid pitch: must be 0-127');
}
if (typeof velocity !== 'number' || velocity < 0 || velocity > 127) {
throw new Error('Invalid velocity: must be 0-127');
}
return client.send('/note', pitch, velocity);
}Always wait for the server to be listening before sending messages:
import { once } from 'node:events';
const server = new Server(3333, '0.0.0.0');
// Wait for server to be ready
await once(server, 'listening');
// Now safe to send messages
console.log('Server ready!');When sending multiple independent messages, use Promise.all for better performance:
// Send multiple messages in parallel
await Promise.all([
client.send('/track/1/volume', 0.8),
client.send('/track/2/volume', 0.6),
client.send('/track/3/volume', 0.9)
]);Possible causes and solutions:
-
Firewall blocking UDP traffic
- Check your firewall settings
- Ensure the UDP port is open
- Try with localhost first (
127.0.0.1)
-
Wrong host binding
- Server: Use
'0.0.0.0'to listen on all interfaces - Server: Use
'127.0.0.1'for localhost only - Client: Match the server's IP address
- Server: Use
-
Port mismatch
- Ensure client and server use the same port number
- Check if another process is using the port
-
Network connectivity
- Test with localhost first (
127.0.0.1) - Verify network connectivity between machines
- Check if devices are on the same network
- Test with localhost first (
This error occurs when trying to send after closing the client:
// ❌ Wrong - Sending after close
await client.close();
await client.send('/test', 123); // Error!
// ✅ Correct - Send before close
await client.send('/test', 123);
await client.close();Ensure you wait for the server to start before sending messages:
import { once } from 'node:events';
const server = new Server(3333, '0.0.0.0');
// Wait for server to be ready
await once(server, 'listening');
// Now safe to send messages
console.log('Server ready!');UDP is unreliable by design and messages can be lost:
Solutions:
- Use TCP-based OSC if reliability is critical (requires custom implementation)
- Implement acknowledgment messages
- Add retry logic for critical messages
- Use bundles to ensure related messages arrive together
If you're seeing high CPU usage:
- Check for infinite loops in event handlers
- Rate limit message sending if sending many messages
- Use bundles instead of many individual messages
- Close unused connections to free resources
To prevent memory leaks:
- Always close clients and servers when done
- Remove event listeners when no longer needed
- Avoid creating new clients/servers in loops
- Reuse client/server instances when possible
// ✅ Good - Proper cleanup
const server = new Server(3333);
const handler = (msg) => console.log(msg);
server.on('message', handler);
// Later, clean up
server.removeListener('message', handler);
await server.close();The encode and decode functions allow you to use OSC over custom transports:
import { encode, decode, Message } from 'node-osc';
import WebSocket from 'ws';
// WebSocket server
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const oscMsg = decode(data);
console.log('Received:', oscMsg);
});
});
// WebSocket client
const ws = new WebSocket('ws://localhost:8080');
const message = new Message('/test', 123);
ws.send(encode(message));OSC bundles support timetags for scheduling:
// Immediate execution (timetag = 0)
const bundle = new Bundle(['/test', 1], ['/test', 2]);
// Scheduled execution (timetag in OSC time)
const futureTime = Date.now() / 1000 + 5; // 5 seconds from now
const scheduledBundle = new Bundle(futureTime, ['/test', 1]);Note: The server receives the timetag but does not automatically schedule execution. You must implement scheduling logic if needed.
For high-throughput applications:
- Reuse client instances instead of creating new ones
- Use bundles to send multiple messages together
- Batch messages and send periodically rather than immediately
- Use binary blobs for large data instead of many arguments
- Profile your code to identify bottlenecks
// ✅ Good - Reuse client
const client = new Client('127.0.0.1', 3333);
for (let i = 0; i < 1000; i++) {
await client.send('/test', i);
}
await client.close();
// ❌ Bad - Creating many clients
for (let i = 0; i < 1000; i++) {
const client = new Client('127.0.0.1', 3333);
await client.send('/test', i);
await client.close();
}- API Documentation - Complete API reference
- OSC Specification - Official OSC 1.0 specification
- Examples - Working code examples
- Main README - Quick start guide