Overview
Robust error handling ensures your integration remains reliable and provides a great user experience even when things go wrong.Error Response Format
All Sticker API errors follow a consistent format:Copy
{
"error": "Human-readable error message",
"code": "MACHINE_READABLE_CODE",
"details": "Additional context (optional)",
"field": "problematic_field (for validation errors)"
}
HTTP Status Codes
4xx Client Errors
Errors caused by the requestUsually don’t retry
5xx Server Errors
Errors on Sticker’s sideSafe to retry with backoff
Common Error Codes
Authentication Errors (401, 403)
Copy
{
"error": "Invalid API key",
"code": "INVALID_API_KEY"
}
- Verify your API key is correct
- Check signature generation logic
- Ensure key hasn’t been revoked
- Don’t retry without fixing the issue
Validation Errors (400)
Copy
{
"error": "Missing required field: organization.email",
"code": "INVALID_REQUEST",
"field": "organization.email"
}
- Validate input before sending
- Show field-specific error messages
- Don’t retry without fixing validation issues
Resource Errors (404, 409)
Copy
{
"error": "No profile found with partner_org_id: acme_123",
"code": "ORGANIZATION_NOT_FOUND",
"partner_org_id": "acme_123"
}
- For 404: Ensure setup was completed first
- For 409: Use existing resource ID
- Check your data consistency
Rate Limiting (429)
Copy
{
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"retry_after": 60
}
- Implement exponential backoff
- Respect
retry_afterheader - Cache organization data to reduce calls
- Consider request throttling
Server Errors (500, 503)
Copy
{
"error": "Internal server error",
"code": "INTERNAL_ERROR",
"details": "Database connection timeout"
}
- Retry with exponential backoff
- Log error for investigation
- Show user-friendly message
- Alert on persistent failures
Retry Logic
Exponential Backoff
Implement smart retries for transient failures:Copy
async function callStickerWithRetry(
endpoint,
options,
maxRetries = 3
) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(endpoint, options);
// Don't retry client errors (except 429)
if (response.status >= 400 && response.status < 500) {
if (response.status === 429) {
// Handle rate limiting
const retryAfter = response.headers.get('Retry-After') || 60;
await sleep(parseInt(retryAfter) * 1000);
continue;
}
// Other 4xx errors - don't retry
throw new Error(`Client error: ${response.status}`);
}
// Server error - will retry
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
// Don't retry on last attempt
if (attempt === maxRetries - 1) {
break;
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt) * 1000;
console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
await sleep(delay);
}
}
throw lastError;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Retry Decision Tree
Copy
Error Occurred
├─ Network Error? → Retry with backoff
├─ Timeout? → Retry with backoff
├─ 429 Rate Limit? → Wait retry_after, then retry
├─ 5xx Server Error? → Retry with backoff
├─ 401/403 Auth Error? → DON'T retry, fix credentials
├─ 400 Validation Error? → DON'T retry, fix request
└─ 404/409 Resource Error? → DON'T retry, fix data
User-Friendly Error Messages
Transform technical errors into helpful messages:Copy
function getUserMessage(error) {
const messages = {
// Authentication
'INVALID_API_KEY': 'Connection error. Please contact support.',
'INVALID_SIGNATURE': 'Authentication failed. Please try again.',
// Resources
'ORGANIZATION_NOT_FOUND': 'Your organization needs to be set up. Please contact your administrator.',
'USER_NOT_FOUND': 'You don\'t have access to supplies. Please contact your administrator.',
// Validation
'INVALID_EMAIL': 'Please check your email address.',
'INVALID_REQUEST': 'Some information is missing. Please check and try again.',
// System
'RATE_LIMIT_EXCEEDED': 'Too many requests. Please wait a moment.',
'INTERNAL_ERROR': 'Something went wrong on our end. Please try again.',
'SERVICE_UNAVAILABLE': 'Service temporarily unavailable. Please try again in a few minutes.',
// Session
'SESSION_EXPIRED': 'Your session expired. Refreshing...',
'SESSION_INVALID': 'Invalid session. Please reload the page.',
};
return messages[error.code] || 'An error occurred. Please try again or contact support.';
}
Error Handling Patterns
Organization Setup
Copy
async function setupOrganization(orgData) {
try {
const result = await callStickerAPI(
'/api/partner/organization-setup',
orgData
);
// Success
return {
success: true,
profileId: result.profile.id
};
} catch (error) {
// Log for debugging
console.error('Organization setup failed:', error);
// Handle specific errors
if (error.code === 'DUPLICATE_ORGANIZATION') {
// Organization already exists, that's okay
return {
success: true,
profileId: error.existing_profile_id,
message: 'Organization already exists'
};
}
if (error.code === 'INVALID_EMAIL') {
return {
success: false,
error: 'Invalid email address',
field: error.field
};
}
if (error.code === 'INVALID_REQUEST') {
return {
success: false,
error: `Missing required information: ${error.field}`,
field: error.field
};
}
// Unknown error
return {
success: false,
error: 'Failed to set up organization. Please try again.',
canRetry: true
};
}
}
User Handshake
Copy
async function authenticateUser(orgId, user) {
try {
const result = await callStickerAPI(
'/api/partner/handshake',
{ partner_org_id: orgId, user }
);
return {
success: true,
sessionKey: result.session_key,
iframeUrl: result.iframe_url
};
} catch (error) {
console.error('Handshake failed:', error);
// Organization not found
if (error.code === 'ORGANIZATION_NOT_FOUND') {
// Try to set up organization automatically
await setupOrganization(orgData);
// Retry handshake
return authenticateUser(orgId, user);
}
// User not authorized
if (error.code === 'USER_NOT_FOUND') {
return {
success: false,
error: 'You don\'t have access. Contact your admin.',
requiresSetup: true
};
}
// Session error
if (error.code === 'SESSION_EXPIRED') {
return {
success: false,
error: 'Session expired. Refreshing...',
shouldRetry: true
};
}
// Rate limited
if (error.code === 'RATE_LIMIT_EXCEEDED') {
return {
success: false,
error: 'Too many requests. Please wait.',
retryAfter: error.retry_after
};
}
// Generic error
return {
success: false,
error: getUserMessage(error),
canRetry: true
};
}
}
iframe Loading
Copy
function StickerEmbed({ orgId, user }) {
const [state, setState] = useState('loading');
const [error, setError] = useState(null);
const [sessionKey, setSessionKey] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const loadSupplies = async () => {
setState('loading');
setError(null);
try {
const result = await authenticateUser(orgId, user);
if (!result.success) {
setError(result.error);
setState('error');
// Auto-retry for certain errors
if (result.shouldRetry && retryCount < 3) {
setTimeout(() => {
setRetryCount(retryCount + 1);
loadSupplies();
}, 2000 * (retryCount + 1)); // Exponential backoff
}
return;
}
setSessionKey(result.sessionKey);
setState('ready');
} catch (error) {
console.error('Failed to load supplies:', error);
setError('Failed to load supplies. Please try again.');
setState('error');
}
};
useEffect(() => {
loadSupplies();
}, []);
if (state === 'loading') {
return (
<div className="loading-state">
<Spinner />
<p>Loading supplies...</p>
</div>
);
}
if (state === 'error') {
return (
<div className="error-state">
<ErrorIcon />
<p>{error}</p>
<button onClick={() => {
setRetryCount(0);
loadSupplies();
}}>
Try Again
</button>
</div>
);
}
return (
<iframe
src={`https://app.sticker.com/embed?session_key=${sessionKey}`}
width="100%"
height="800px"
/>
);
}
Error Logging
Structured Logging
Log errors with context for debugging:Copy
function logError(context, error, additionalInfo = {}) {
const errorLog = {
timestamp: new Date().toISOString(),
context: context,
error: {
message: error.message,
code: error.code,
stack: error.stack
},
api: {
endpoint: additionalInfo.endpoint,
method: additionalInfo.method,
status: additionalInfo.status
},
user: {
org_id: additionalInfo.orgId,
email: additionalInfo.userEmail
},
...additionalInfo
};
// Send to your logging service
logger.error('Sticker Integration Error', errorLog);
// Alert on critical errors
if (error.code === 'INTERNAL_ERROR' || error.status >= 500) {
alertOps('Sticker API error', errorLog);
}
}
Error Metrics
Track error rates and patterns:Copy
const errorMetrics = {
total: 0,
byCode: {},
byEndpoint: {}
};
function trackError(error, endpoint) {
errorMetrics.total++;
errorMetrics.byCode[error.code] = (errorMetrics.byCode[error.code] || 0) + 1;
errorMetrics.byEndpoint[endpoint] = (errorMetrics.byEndpoint[endpoint] || 0) + 1;
// Alert if error rate is high
const errorRate = errorMetrics.total / totalRequests;
if (errorRate > 0.05) { // 5% error rate
alertOps('High error rate in Sticker integration', {
error_rate: errorRate,
total_errors: errorMetrics.total,
by_code: errorMetrics.byCode
});
}
}
Fallback Strategies
Graceful Degradation
Provide alternatives when integration fails:Copy
function SuppliesSection({ orgId, user }) {
const [integrationHealthy, setIntegrationHealthy] = useState(true);
const [consecutiveFailures, setConsecutiveFailures] = useState(0);
const checkHealth = async () => {
try {
await fetch('https://api.sticker.com/health');
setConsecutiveFailures(0);
} catch (error) {
setConsecutiveFailures(prev => prev + 1);
// After 3 failures, show fallback
if (consecutiveFailures >= 3) {
setIntegrationHealthy(false);
}
}
};
if (!integrationHealthy) {
return (
<div className="fallback-ui">
<h3>Supplies Currently Unavailable</h3>
<p>Our supplies system is temporarily unavailable.</p>
<div className="alternatives">
<button onClick={() => window.location.href = 'tel:18005551234'}>
Call to Order: 1-800-555-1234
</button>
<button onClick={() => window.location.href = 'mailto:orders@sticker.com'}>
Email Your Order
</button>
<button onClick={() => setIntegrationHealthy(true)}>
Try Again
</button>
</div>
</div>
);
}
return <StickerEmbed orgId={orgId} user={user} />;
}
Circuit Breaker
Prevent cascading failures:Copy
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
// Usage
const breaker = new CircuitBreaker();
try {
const result = await breaker.call(() =>
callStickerAPI('/api/partner/handshake', data)
);
} catch (error) {
if (error.message === 'Circuit breaker is OPEN') {
showFallbackUI();
}
}
Testing Error Handling
Test how your integration handles errors:Copy
describe('Error Handling', () => {
it('should retry on 500 errors', async () => {
mockAPI.fail(500, 2); // Fail twice with 500
const result = await callStickerWithRetry(endpoint, options);
expect(result).toBeDefined();
expect(mockAPI.callCount).toBe(3);
});
it('should not retry on 400 errors', async () => {
mockAPI.fail(400);
await expect(callStickerWithRetry(endpoint, options))
.rejects.toThrow();
expect(mockAPI.callCount).toBe(1);
});
it('should show user-friendly messages', () => {
const error = { code: 'ORGANIZATION_NOT_FOUND' };
const message = getUserMessage(error);
expect(message).not.toContain('ORGANIZATION_NOT_FOUND');
expect(message).toContain('organization');
});
});

