> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/iii-hq/sdk/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Error handling patterns, try/catch blocks, and error types in the III SDK

## 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:

<CodeGroup>
  ```typescript Node.js theme={null}
  // 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)
  }
  ```

  ```python Python theme={null}
  # 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))
  ```
</CodeGroup>

### Registration Errors

<CodeGroup>
  ```typescript Node.js theme={null}
  // 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
  }
  ```

  ```python Python theme={null}
  # 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
  ```
</CodeGroup>

### Trigger Registration Errors

<CodeGroup>
  ```typescript Node.js theme={null}
  // 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) => {},
    }
  )
  ```

  ```python Python theme={null}
  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())
  ```
</CodeGroup>

## Function Handler Error Handling

### Try/Catch in Handlers

<Steps>
  <Step title="Wrap handler logic in try/catch">
    Catch errors inside your function handlers to return meaningful responses:

    <CodeGroup>
      ```typescript Node.js theme={null}
      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' }
        }
      })
      ```

      ```python Python theme={null}
      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)
      ```
    </CodeGroup>
  </Step>

  <Step title="Log errors with context">
    Use the context logger to attach trace IDs and span IDs automatically:

    <CodeGroup>
      ```typescript Node.js theme={null}
      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
        }
      })
      ```

      ```python Python theme={null}
      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
      ```
    </CodeGroup>
  </Step>
</Steps>

### Uncaught Errors

<Warning>
  If a handler throws an uncaught error, the SDK automatically catches it and returns an error response to the caller:

  <CodeGroup>
    ```typescript Node.js theme={null}
    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'
    //   }
    // }
    ```

    ```python Python theme={null}
    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'
    #   }
    # }
    ```
  </CodeGroup>
</Warning>

## Invocation Error Handling

### Configuring Timeouts

<CodeGroup>
  ```typescript Node.js theme={null}
  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')
    }
  }
  ```

  ```python Python theme={null}
  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')
  ```
</CodeGroup>

### Handling Connection Errors

<CodeGroup>
  ```typescript Node.js theme={null}
  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)
  ```

  ```python Python theme={null}
  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)
  ```
</CodeGroup>

## Best Practices

<Tip>
  **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)
</Tip>

### Graceful Degradation

<CodeGroup>
  ```typescript Node.js theme={null}
  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 })
    }
  })
  ```

  ```python Python theme={null}
  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)
  ```
</CodeGroup>

## Shutdown Error Handling

<CodeGroup>
  ```typescript Node.js theme={null}
  // 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)
  })
  ```

  ```python Python theme={null}
  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)))
  ```
</CodeGroup>
