Error Handling
Learn how to handle errors effectively when using the Conviso Node SDK.
Overview
The Conviso Node SDK uses async/await and throws errors for failed operations. All methods return Promises that may reject with errors.
Error Types
Validation Errors
Thrown when request parameters are invalid:
try {
// Empty identifier
await client.identify('', { email: '[email protected]' });
} catch (error) {
console.error(error.message); // "Identifier cannot be empty"
}
CDPEmailError
Thrown by sendEmail() when email delivery fails:
try {
await client.sendEmail({
to: '[email protected]',
identifiers: { id: 'user123' },
transactional_message_id: 'MISSING_TEMPLATE'
});
} catch (error) {
console.error({
name: error.name, // 'CDPEmailError'
code: error.code, // 'EMAIL_SEND_FAILED'
message: error.message, // Error description
status: error.status, // HTTP status code (e.g., 404)
summary: error.summary // Detailed error info
});
}
CDPPushError
Thrown by sendPush() when push delivery fails:
try {
await client.sendPush({
identifiers: { id: 'user123' },
transactional_message_id: 'MISSING_TEMPLATE',
title: 'Test',
body: 'Test'
});
} catch (error) {
console.error({
name: error.name, // 'CDPPushError'
code: error.code, // 'PUSH_SEND_FAILED'
message: error.message, // Error description
status: error.status, // HTTP status code
summary: error.summary // Detailed error info
});
}
Network Errors
Axios network errors for connection issues:
try {
await client.identify('user123', { email: '[email protected]' });
} catch (error) {
if (error.code === 'ECONNABORTED') {
console.error('Request timeout');
} else if (error.code === 'ENOTFOUND') {
console.error('DNS lookup failed - check endpoint');
} else {
console.error('Network error:', error.message);
}
}
Common Error Scenarios
Invalid API Key
try {
await client.identify('user123', { email: '[email protected]' });
} catch (error) {
if (error.response?.status === 401) {
console.error('Invalid API key - check your credentials');
}
}
Template Not Found
try {
await client.sendEmail({
to: '[email protected]',
identifiers: { id: 'user123' },
transactional_message_id: 'NONEXISTENT_TEMPLATE'
});
} catch (error) {
if (error.status === 404) {
console.error('Email template not found');
}
}
Rate Limiting
try {
await client.track('user123', 'event_name', { ... });
} catch (error) {
if (error.response?.status === 429) {
console.error('Rate limit exceeded - retry later');
// Implement exponential backoff
}
}
Validation Failures
try {
await client.sendEmail({
to: 'invalid-email',
identifiers: { id: 'user123' },
transactional_message_id: 'WELCOME'
});
} catch (error) {
if (error.message.includes('Invalid email address format')) {
console.error('Email validation failed');
}
}
Timeout
try {
await client.identify('user123', { email: '[email protected]' });
} catch (error) {
if (error.code === 'ECONNABORTED') {
console.error('Request timed out');
// Retry or queue for later
}
}
Error Handling Patterns
Basic Try-Catch
try {
await client.identify('user123', {
email: '[email protected]',
name: 'John Doe'
});
console.log('User identified successfully');
} catch (error) {
console.error('Failed to identify user:', error.message);
}
Comprehensive Error Handler
async function identifyUser(userId: string, properties: Record<string, any>) {
try {
await client.identify(userId, properties);
return { success: true };
} catch (error) {
// Log error with context
logger.error('Failed to identify user', {
userId,
error: error.message,
status: error.response?.status,
stack: error.stack
});
// Handle specific errors
if (error.message.includes('Identifier cannot be empty')) {
return { success: false, error: 'Invalid user ID' };
}
if (error.response?.status === 401) {
return { success: false, error: 'Authentication failed' };
}
if (error.response?.status === 429) {
return { success: false, error: 'Rate limit exceeded', retryable: true };
}
if (error.code === 'ECONNABORTED') {
return { success: false, error: 'Request timeout', retryable: true };
}
// Generic error
return { success: false, error: 'Failed to identify user' };
}
}
Fire and Forget with Error Logging
For non-critical operations:
// Don't block on the operation
client.track('user123', 'page_viewed', { page: '/home' })
.catch(error => {
logger.error('Failed to track event', {
userId: 'user123',
event: 'page_viewed',
error: error.message
});
});
// Continue immediately
res.render('home');
Retry Logic
async function identifyWithRetry(
userId: string,
properties: Record<string, any>,
maxRetries = 3
) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await client.identify(userId, properties);
return { success: true };
} catch (error) {
lastError = error;
// Don't retry validation errors
if (error.message.includes('cannot be empty') ||
error.message.includes('Invalid')) {
throw error;
}
// Don't retry auth errors
if (error.response?.status === 401) {
throw error;
}
// Retry on timeout or 5xx errors
const shouldRetry =
error.code === 'ECONNABORTED' ||
error.response?.status >= 500 ||
error.response?.status === 429;
if (!shouldRetry || attempt === maxRetries) {
break;
}
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
logger.warn(`Retrying identify (attempt ${attempt + 1}/${maxRetries})`);
}
}
throw lastError;
}
Graceful Degradation
Continue operation even if Conviso call fails:
app.post('/api/signup', async (req, res) => {
const { email, name } = req.body;
try {
// Critical: Create user in database
const user = await db.users.create({ email, name });
// Non-critical: Identify in CDP
try {
await client.identify(user.id, { email, name });
} catch (cdpError) {
// Log but don't fail the signup
logger.error('Conviso identify failed during signup', {
userId: user.id,
error: cdpError.message
});
// Queue for retry
await retryQueue.add('identify', {
userId: user.id,
properties: { email, name }
});
}
res.json({ success: true, userId: user.id });
} catch (error) {
res.status(500).json({ error: 'Signup failed' });
}
});
Circuit Breaker
Prevent cascading failures:
import CircuitBreaker from 'opossum';
const identifyBreaker = new CircuitBreaker(
(userId: string, properties: Record<string, any>) =>
client.identify(userId, properties),
{
timeout: 5000,
errorThresholdPercentage: 50,
resetTimeout: 30000
}
);
identifyBreaker.fallback(() => {
logger.warn('Circuit breaker open - skipping identify');
return { success: false, circuitOpen: true };
});
identifyBreaker.on('open', () => {
logger.error('Circuit breaker opened for identify');
alerting.notify('cdp_circuit_open');
});
// Use the circuit breaker
try {
await identifyBreaker.fire('user123', { email: '[email protected]' });
} catch (error) {
console.error('Identify failed:', error);
}
Logging Best Practices
Structured Logging
import pino from 'pino';
const logger = pino();
try {
await client.sendEmail({
to: '[email protected]',
identifiers: { id: 'user123' },
transactional_message_id: 'WELCOME'
});
logger.info({
event: 'email_sent',
userId: 'user123',
template: 'WELCOME',
to: '[email protected]'
});
} catch (error) {
logger.error({
event: 'email_send_failed',
userId: 'user123',
template: 'WELCOME',
to: '[email protected]',
error: error.message,
status: error.status,
code: error.code
});
}
Custom Logger Integration
const client = new CDPClient({
cdpApiKey: process.env.CDP_API_KEY,
cdpLogger: {
debug: (msg) => logger.debug(msg),
error: (msg, ctx) => {
logger.error(ctx, msg);
// Send to error tracking
if (ctx?.status >= 500) {
Sentry.captureMessage(msg, {
level: 'error',
extra: ctx
});
}
},
warn: (msg) => logger.warn(msg)
}
});
Monitoring and Alerting
Track Error Rates
import { metrics } from './monitoring';
async function trackWithMetrics(
userId: string,
eventName: string,
properties: Record<string, any>
) {
const startTime = Date.now();
try {
await client.track(userId, eventName, properties);
metrics.increment('cdp.track.success');
metrics.timing('cdp.track.duration', Date.now() - startTime);
} catch (error) {
metrics.increment('cdp.track.error');
metrics.increment(`cdp.track.error.${error.response?.status || 'unknown'}`);
// Alert on high error rates
const errorRate = await metrics.getRate('cdp.track.error');
if (errorRate > 0.05) { // 5% error rate
alerting.notify('high_cdp_error_rate', { rate: errorRate });
}
throw error;
}
}
Error Tracking Integration
import * as Sentry from '@sentry/node';
async function sendEmailWithTracking(emailRequest: SendEmailRequest) {
try {
const result = await client.sendEmail(emailRequest);
return { success: true, result };
} catch (error) {
// Capture in Sentry
Sentry.captureException(error, {
tags: {
operation: 'send_email',
template: emailRequest.message.transactional_message_id
},
extra: {
to: emailRequest.message.to,
identifiers: emailRequest.message.identifiers,
status: error.status,
code: error.code
}
});
return { success: false, error: error.message };
}
}
Testing Error Scenarios
Unit Tests
import { CDPClient } from '@codematic.io/cdp-node';
import nock from 'nock';
describe('CDPClient Error Handling', () => {
let client: CDPClient;
beforeEach(() => {
client = new CDPClient({
cdpApiKey: 'test-key',
cdpEndpoint: 'https://api.test.com'
});
});
test('throws validation error for empty identifier', async () => {
await expect(
client.identify('', { email: '[email protected]' })
).rejects.toThrow('Identifier cannot be empty');
});
test('handles 401 authentication error', async () => {
nock('https://api.test.com')
.post('/v1/persons/identify')
.reply(401, { error: 'Invalid API key' });
await expect(
client.identify('user123', { email: '[email protected]' })
).rejects.toMatchObject({
response: {
status: 401
}
});
});
test('handles network timeout', async () => {
nock('https://api.test.com')
.post('/v1/persons/identify')
.delayConnection(11000) // Longer than timeout
.reply(200);
await expect(
client.identify('user123', { email: '[email protected]' })
).rejects.toMatchObject({
code: 'ECONNABORTED'
});
});
});
Debug Mode
Enable debug mode to see detailed error information:
const client = new CDPClient({
cdpApiKey: process.env.CDP_API_KEY,
debug: true // See all errors with stack traces
});
try {
await client.identify('user123', { email: '[email protected]' });
} catch (error) {
// Debug mode logs detailed error info automatically
console.error('Operation failed');
}