ERROR_HANDLING_PATTERN.md•6.19 kB
# Error Handling Pattern - Opción A (Híbrido)
## Principio
**Errores técnicos → `raise Exception`**
**Estados de negocio → `return dict` con "success"**
---
## 1. Errores Técnicos (usar `raise`)
Situaciones donde algo técnicamente falló y no se puede continuar:
### Ejemplos:
- ❌ API de OCI no responde
- ❌ Permisos insuficientes (401/403)
- ❌ Recurso no encontrado (404)
- ❌ Parámetros inválidos
- ❌ Timeout de red
- ❌ Configuración OCI incorrecta
### Código:
```python
def get_instance(compute_client, instance_id):
    """Get instance details."""
    # Si falla, lanza ServiceError automáticamente
    instance = compute_client.get_instance(instance_id).data
    return {
        "id": instance.id,
        "name": instance.display_name,
        "state": instance.lifecycle_state,
        ...
    }
    # No try/except - dejamos que la excepción se propague
```
---
## 2. Estados de Negocio (usar `return dict`)
Situaciones válidas del sistema que requieren acción del usuario:
### Ejemplos:
- ✅ Instancia ya está corriendo (intentar start)
- ✅ Instancia no puede iniciarse desde estado PROVISIONING
- ✅ DB System ya está detenido
- ⚠️ Operación en progreso, verificar más tarde
### Código:
```python
def start_instance(compute_client, instance_id):
    """Start an instance."""
    # Error técnico: si no existe, lanza excepción
    instance = compute_client.get_instance(instance_id).data
    # Estado de negocio: ya está corriendo
    if instance.lifecycle_state == "RUNNING":
        return {
            "success": True,
            "already_running": True,
            "message": "Instance is already running",
            "current_state": "RUNNING"
        }
    # Estado de negocio: no se puede iniciar
    if instance.lifecycle_state not in ["STOPPED"]:
        return {
            "success": False,
            "message": f"Cannot start instance from state {instance.lifecycle_state}",
            "current_state": instance.lifecycle_state
        }
    # Operación real - puede lanzar excepción
    compute_client.instance_action(instance_id, "START")
    return {
        "success": True,
        "message": "Instance is starting",
        "current_state": "STARTING"
    }
```
---
## 3. Flujo Completo
```
Usuario
  ↓
MCP Tool (mcp_server.py)
  ↓
@mcp_tool_wrapper  ← Captura excepciones, convierte a {"error": ...}
  ↓
Tool Function (tools/*.py)
  ├─→ Error técnico → raise Exception
  └─→ Estado de negocio → return {"success": bool, ...}
  ↓
Decorator procesa:
  ├─→ Exception → return {"error": "..."}
  └─→ Dict → return as-is
  ↓
Cliente MCP recibe respuesta uniforme
```
---
## 4. Estructura de Respuestas
### Respuesta Exitosa (dato único):
```python
{
    "id": "ocid1...",
    "name": "my-instance",
    "state": "RUNNING",
    ...
}
```
### Respuesta Exitosa (lista):
```python
[
    {"id": "ocid1...", "name": "instance1"},
    {"id": "ocid2...", "name": "instance2"}
]
```
### Operación con Estado de Negocio:
```python
{
    "success": True,
    "message": "Instance is starting",
    "current_state": "STARTING",
    "instance_id": "ocid1...",  # opcional
}
```
### Error Técnico (convertido por decorator):
```python
{
    "error": "ServiceError: 404 NotFound - Instance not found"
}
```
---
## 5. Checklist para Desarrolladores
Al escribir una función tool:
- [ ] **Validaciones de parámetros**: Lanzar `ValueError` si parámetros inválidos
- [ ] **Llamadas OCI**: NO usar try/except, dejar que excepciones se propaguen
- [ ] **Estados de negocio**: Verificar condiciones y retornar dict con "success"
- [ ] **Logging**: Solo `logger.info()` para operaciones exitosas
- [ ] **No loguear excepciones**: El decorador ya lo hace
### Ejemplo Correcto:
```python
def terminate_instance(compute_client, instance_id, preserve_boot_volume=False):
    """Terminate an instance."""
    # Validación - error técnico
    if not instance_id:
        raise ValueError("instance_id is required")
    # Obtener instancia - puede lanzar ServiceError (404, 401, etc)
    instance = compute_client.get_instance(instance_id).data
    # Estado de negocio
    if instance.lifecycle_state == "TERMINATED":
        return {
            "success": True,
            "already_terminated": True,
            "message": "Instance already terminated"
        }
    # Operación - puede lanzar excepción
    compute_client.terminate_instance(instance_id, preserve_boot_volume)
    logger.info(f"Initiated termination of instance {instance_id}")
    return {
        "success": True,
        "message": "Instance termination initiated",
        "instance_id": instance_id
    }
```
---
## 6. Tipos de Excepciones OCI Comunes
- `oci.exceptions.ServiceError`: Error de API OCI (404, 401, 403, 500, etc)
- `oci.exceptions.RequestException`: Error de red/timeout
- `oci.exceptions.ConfigFileNotFound`: Configuración OCI no encontrada
- `oci.exceptions.InvalidConfig`: Configuración OCI inválida
**Todas deben propagarse** (no capturar)
---
## 7. Anti-patrones a Evitar
### ❌ MAL: Capturar y retornar error dict
```python
def get_instance(client, instance_id):
    try:
        instance = client.get_instance(instance_id).data
        return {"success": True, "data": instance}
    except Exception as e:
        return {"success": False, "error": str(e)}  # ❌ NO!
```
### ✅ BIEN: Dejar que excepción se propague
```python
def get_instance(client, instance_id):
    instance = client.get_instance(instance_id).data
    return {
        "id": instance.id,
        "name": instance.display_name,
        ...
    }
    # Si falla, el decorador captura la excepción
```
---
## 8. Testing
### Test Error Técnico:
```python
def test_get_instance_not_found():
    with pytest.raises(oci.exceptions.ServiceError) as exc:
        get_instance(mock_client, "invalid-id")
    assert exc.value.status == 404
```
### Test Estado de Negocio:
```python
def test_start_instance_already_running():
    result = start_instance(mock_client, "running-instance-id")
    assert result["success"] is True
    assert result["already_running"] is True
```