Overview
The III SDK provides structured error handling across function invocations, WebSocket connections, and trigger registrations. Understanding error types and handling patterns ensures robust distributed applications.Error Types
Invocation Errors
When calling remote functions, the SDK returns structured error objects:// Error structure from InvocationResultMessage
type InvocationError = {
code: 'invocation_failed' | 'function_not_found'
message: string
}
// Timeout error
try {
await iii.call('my-function', { data: 'value' }, 5000)
} catch (error) {
// Error: Invocation timeout after 5000ms: my-function
console.error(error.message)
}
# Error codes
class InvocationError:
code: str # 'invocation_failed' | 'function_not_found'
message: str
# Timeout error
try:
result = await iii.trigger('my-function', {'data': 'value'}, timeout=5.0)
except TimeoutError as e:
# TimeoutError: Invocation of 'my-function' timed out after 5.0s
logger.error(str(e))
Registration Errors
// Duplicate function ID
try {
iii.registerFunction({ id: 'users::get' }, handler)
iii.registerFunction({ id: 'users::get' }, handler) // throws
} catch (error) {
// Error: function id already registered: users::get
}
// Missing required fields
try {
iii.registerFunction({ id: '' }, handler)
} catch (error) {
// Error: id is required
}
# Duplicate function ID
try:
iii.register_function('users::get', handler)
iii.register_function('users::get', handler) # raises
except ValueError as e:
# ValueError: function id 'users::get' already registered
pass
# Missing required fields
try:
iii.register_function('', handler)
except ValueError as e:
# ValueError: id is required
pass
Trigger Registration Errors
// Trigger handler errors are communicated via TriggerRegistrationResult
iii.registerTriggerType(
{ id: 'cron', description: 'Cron trigger' },
{
registerTrigger: async (config) => {
if (!config.config.schedule) {
throw new Error('schedule is required')
}
// Error propagated to engine as:
// { code: 'trigger_registration_failed', message: 'schedule is required' }
},
unregisterTrigger: async (config) => {},
}
)
from iii import TriggerHandler, TriggerConfig
class CronHandler(TriggerHandler):
async def register_trigger(self, config: TriggerConfig) -> None:
if not config.config.get('schedule'):
raise ValueError('schedule is required')
# Error propagated to engine as:
# {'code': 'trigger_registration_failed', 'message': 'schedule is required'}
async def unregister_trigger(self, config: TriggerConfig) -> None:
pass
iii.register_trigger_type('cron', 'Cron trigger', CronHandler())
Function Handler Error Handling
Try/Catch in Handlers
1
Wrap handler logic in try/catch
Catch errors inside your function handlers to return meaningful responses:
import { getContext } from 'iii-sdk'
iii.registerFunction({ id: 'users::create' }, async (input) => {
const { logger } = getContext()
try {
// Validate input
if (!input.email) {
logger.warn('Missing email field', { input })
return { error: 'email is required' }
}
// Perform operation
const user = await db.createUser(input)
logger.info('User created', { userId: user.id })
return { success: true, user }
} catch (error) {
logger.error('Failed to create user', { error: error.message, input })
// Return structured error response
if (error.code === 'DUPLICATE_KEY') {
return { error: 'User already exists' }
}
return { error: 'Internal server error' }
}
})
from iii import get_context
async def create_user(input_data: dict) -> dict:
ctx = get_context()
try:
# Validate input
if not input_data.get('email'):
ctx.logger.warn('Missing email field', input_data)
return {'error': 'email is required'}
# Perform operation
user = await db.create_user(input_data)
ctx.logger.info('User created', {'user_id': user.id})
return {'success': True, 'user': user}
except Exception as error:
ctx.logger.error('Failed to create user', {'error': str(error), 'input': input_data})
# Return structured error response
if hasattr(error, 'code') and error.code == 'DUPLICATE_KEY':
return {'error': 'User already exists'}
return {'error': 'Internal server error'}
iii.register_function('users::create', create_user)
2
Log errors with context
Use the context logger to attach trace IDs and span IDs automatically:
iii.registerFunction({ id: 'process::payment' }, async (input) => {
const { logger, trace } = getContext()
try {
const result = await stripe.charge(input.amount)
return result
} catch (error) {
// Automatically includes trace_id, span_id, function_id
logger.error('Payment failed', {
amount: input.amount,
stripeError: error.code
})
// Add error details to span
trace?.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
trace?.recordException(error)
throw error // Re-throw to return error to caller
}
})
async def process_payment(input_data: dict) -> dict:
ctx = get_context()
try:
result = await stripe.charge(input_data['amount'])
return result
except Exception as error:
# Automatically includes trace context
ctx.logger.error('Payment failed', {
'amount': input_data['amount'],
'stripe_error': getattr(error, 'code', None)
})
# Add error details to span (if OTel enabled)
if ctx.trace:
from opentelemetry.trace import StatusCode
ctx.trace.set_status(StatusCode.ERROR, str(error))
ctx.trace.record_exception(error)
raise # Re-raise to return error to caller
Uncaught Errors
If a handler throws an uncaught error, the SDK automatically catches it and returns an error response to the caller:
iii.registerFunction({ id: 'divide' }, async ({ a, b }) => {
return a / b // Will throw if not numbers
})
// Caller receives:
// {
// error: {
// code: 'invocation_failed',
// message: 'Cannot read property of undefined'
// }
// }
async def divide(data: dict) -> float:
return data['a'] / data['b'] # Will raise if missing keys or zero
iii.register_function('divide', divide)
# Caller receives:
# {
# 'error': {
# 'code': 'invocation_failed',
# 'message': 'division by zero'
# }
# }
Invocation Error Handling
Configuring Timeouts
import { init, DEFAULT_INVOCATION_TIMEOUT_MS } from 'iii-sdk'
// Set default timeout for all invocations
const iii = init('ws://localhost:49134', {
invocationTimeoutMs: 10000 // 10 seconds
})
// Override per-invocation
try {
const result = await iii.call('slow-function', data, 30000) // 30 seconds
} catch (error) {
if (error.message.includes('timeout')) {
console.error('Function took too long')
}
}
from iii import III, InitOptions, DEFAULT_INVOCATION_TIMEOUT_MS
# Set default timeout for all invocations
iii = III('ws://localhost:49134', InitOptions(
invocation_timeout_ms=10000 # 10 seconds
))
# Override per-invocation
try:
result = await iii.call('slow-function', data, timeout=30.0) # 30 seconds
except TimeoutError:
print('Function took too long')
Handling Connection Errors
import type { ConnectionStateCallback } from 'iii-sdk'
const handleConnectionState: ConnectionStateCallback = (state) => {
switch (state) {
case 'connected':
console.log('Ready to invoke functions')
break
case 'reconnecting':
console.warn('Connection lost, retrying...')
break
case 'failed':
console.error('Max retries reached, connection failed')
// Implement fallback or alert
break
}
}
iii.onConnectionStateChange(handleConnectionState)
from iii import IIIConnectionState
def handle_connection_state(state: IIIConnectionState) -> None:
if state == 'connected':
print('Ready to invoke functions')
elif state == 'reconnecting':
print('Connection lost, retrying...')
elif state == 'failed':
print('Max retries reached, connection failed')
# Implement fallback or alert
iii.on_connection_state_change(handle_connection_state)
Best Practices
Error Handling Checklist:
- ✅ Always validate input at the start of handlers
- ✅ Use structured error responses (not just strings)
- ✅ Log errors with context using
ctx.logger.error() - ✅ Set appropriate timeouts for long-running operations
- ✅ Handle connection state changes for resilience
- ✅ Record exceptions in spans for distributed tracing
- ✅ Differentiate between client errors (4xx) and server errors (5xx)
Graceful Degradation
iii.registerFunction({ id: 'get-recommendations' }, async (input) => {
const { logger } = getContext()
try {
// Try ML service first
return await iii.call('ml::recommend', input, 5000)
} catch (error) {
logger.warn('ML service unavailable, using fallback', { error: error.message })
// Fallback to simple logic
return await iii.call('db::popular-items', { limit: 10 })
}
})
async def get_recommendations(input_data: dict) -> list:
ctx = get_context()
try:
# Try ML service first
return await iii.call('ml::recommend', input_data, timeout=5.0)
except Exception as error:
ctx.logger.warn('ML service unavailable, using fallback', {'error': str(error)})
# Fallback to simple logic
return await iii.call('db::popular-items', {'limit': 10})
iii.register_function('get-recommendations', get_recommendations)
Shutdown Error Handling
// Graceful shutdown rejects pending invocations
process.on('SIGTERM', async () => {
console.log('Shutting down...')
await iii.shutdown() // Rejects all pending with 'iii is shutting down'
process.exit(0)
})
import signal
async def shutdown(signum, frame):
print('Shutting down...')
await iii.shutdown() # Rejects all pending with 'iii is shutting down'
signal.signal(signal.SIGTERM, lambda s, f: asyncio.create_task(shutdown(s, f)))