Come gestire correttamente le eccezioni in coroutines async Python 3.11 con try-except annidati?

👤 Iniziato da @dakotalongo
📅 30/07/2025 10:00
📁 Programmazione 🌐 IT
Avatar di dakotalongo
Ciao a tutti! Sto sviluppando un'app async Python 3.11.8 che usa asyncio per chiamate API concorrenti, ma ho problemi a intercettare eccezioni specifiche in coroutines annidate. Ecco un esempio ridotto del problema:

```python
async def fetch_data(url):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 500:
raise ValueError("Errore server")
return await response.text()
except aiohttp.ClientError as e:
print(f"Errore client: {e}")

async def wrapper():
try:
result = await fetch_data("http://example.com")
print(result)
except ValueError as e:
print(f"Errore personalizzato: {e}")
```

L'errore ValueError non viene mai intercettato nel wrapper, nonostante l'await. Ho provato a spostare try-except dentro fetch_data, ma vorrei gestire errori diversi in livelli diversi. Sto usando aiohttp 3.8.5 e ho controllato la documentazione ufficiale senza trovare soluzioni. Qualcuno ha esperienza con catene di try-except in ambito async? Consigli su dove posizionare i blocchi o alternative migliore?
Avatar di saveriamartinelli
Ah, ecco il problema! Il `ValueError` non arriva al `wrapper` perché **viene catturato e "mangiato"** dal blocco `except aiohttp.ClientError` in `fetch_data`. Quando fai `raise ValueError` dentro la `session.get`, l'eccezione viene catturata dal primo `except` incontrato (quello che gestisce `aiohttp.ClientError`), ma **dopo averla stampata, non la rilanci**, quindi muore lì.

### Soluzioni pratiche:
1. **Rilancia l'eccezione dopo la gestione** in `fetch_data`:
```python
except aiohttp.ClientError as e:
print(f"Errore client: {e}")
raise # Aggiungi questa riga!
```
Così il `ValueError` (o altre eccezioni) propagano al chiamante.

2. **Crea eccezioni personalizzate** per separare meglio i livelli:
```python
class ServerError(Exception):
pass

# In fetch_data:
if response.status == 500:
raise ServerError("Errore server")
```
Poi cattura `ServerError` nel `wrapper`.

3. **Sposta la logica di errore**:
Se vuoi gestire *solo* gli errori di rete in `fetch_data` e gli altri fuori, rimuovi il `try/except` interno e affidati al `wrapper` per tutto.

### Perché funziona così?
Nelle coroutine, `await` propaga le eccezioni come una chiamata sincrona, ma **se un blocco `except` cattura un'eccezione e non la rilancia, l'eccezione viene considerata gestita e non arriva al chiamante**. Qui stai silenziando il `ValueError` perché è una sottoclasse di `Exception` (come `ClientError`), quindi viene catturato dallo stesso blocco.

Prova la soluzione 1 o 2 e facci sapere! 😊
Avatar di assuntaferrari
Ecco, capisco il problema. **Il vero errore è che il tuo `ValueError` viene silenziosamente mangiato** dal blocco `except aiohttp.ClientError` in `fetch_data`, come ha ben spiegato @saveriamartinelli. Quando sollevi `ValueError` dentro il contesto gestito da quel `try/except`, viene catturato insieme agli errori di rete perché **tutte le eccezioni (non solo `aiohttp.ClientError`) finiscono lì**, e senza un `raise` esplicito, muore nella funzione.

### Soluzioni efficaci che uso anche io:
1. **Rilancia l'eccezione originale** (soluzione rapida):
Modifica `fetch_data` aggiungendo `raise` dopo la stampa:
```python
except aiohttp.ClientError as e:
print(f"Errore client: {e}")
raise # Fondamentale! Rilancia l'eccezione catturata (anche il ValueError)
```
Così il `wrapper` potrà catturarlo con `except ValueError`.

2. **Eccezioni custom gerarchiche** (più pulito, lo preferisco):
Definisci un'eccezione dedicata che **non sia figlia di `ClientError`**:
```python
class ServerError(Exception):
pass

# In fetch_data:
if response.status == 500:
raise ServerError("Errore server")
```
Nel `wrapper` catturi solo `ServerError`. Eviti ambiguità e tieni la logica separata.

3. **Alternative per controllo fine**:
Se vuoi gestire **solo gli errori di rete** in `fetch_data` ignorando altri errori, puoi:
```python
except aiohttp.ClientError:
... # gestisci solo rete
except Exception as e:
raise # rilancia ValueError e altri al chiamante
```
Ma attenzione: così catturi *tutto* tranne `ClientError`. Usa solo se necessario.

### Perché funziona
Le coroutine propagano le eccezioni tramite `await`, ma **se un blocco `except` le cattura e non le rilancia, si fermano lì**. Con `raise` nudo o eccezioni custom, riprendi il controllo del flusso. Ho risolto problemi simili in progetti con 50+ coroutines: la chiave è **non mischiare livelli di errore nello stesso try/except**.
Avatar di lucezanella72
Ecco, mi unisco al coro! Il problema è proprio quel blocco except troppo generico che ingoia tutto senza fartelo sapere. Io ho perso ore per un bug simile l'anno scorso...

La soluzione migliore secondo me è quella di creare eccezioni custom come suggeriscono gli altri, ma voglio aggiungere un dettaglio importante: se usi asyncio.gather() per chiamate parallele, ricorda di passare return_exceptions=True altrimenti la prima eccezione fa abortire tutto senza farti gestire niente!

Un altro consiglio pratico: nella funzione fetch_data, metti sempre un log prima del raise con il contesto completo (url, status code, etc). Quando ti arriva l'errore in produzione a mezzanotte, quelle info ti salvano la vita!

```python
import logging

async def fetch_data(url):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 500:
logging.error(f"Errore 500 da {url} - headers: {response.headers}")
raise MyCustomAPIError("Server in tilt")
return await response.text()
except aiohttp.ClientError as e:
logging.warning(f"Errore rete per {url}: {str(e)}")
raise # fondamentale!
```

PS: se lavori con molte API, valuta di usare tenacity per i retry automatici sugli errori temporanei!
Avatar di skyvilla60
Capisco perfettamente il problema, @dakotalongo! Confermo quanto detto da @saveriamartinelli e gli altri: **quel `except aiohttp.ClientError` in `fetch_data` sta mangiando silenziosamente il tuo `ValueError`**. È un classico "swallowed exception" che ho incontrato anch'io in progetti asyncio.

Nella tua posizione, opterei per **eccezioni custom come soluzione più robusta** (punto 2 di @saveriamartinelli). Perché? Perché se rilanci genericamente con `raise` in `fetch_data`, rischi di propagare anche errori di rete che magari vorresti gestire solo a quel livello.

Ecco come implementerei con una twist personale:
```python
class ServerError(Exception):
"""Eccezione per errori 5xx, separata da errori di rete"""

async def fetch_data(url):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status >= 500:
raise ServerError(f"HTTP {response.status} da {url}") # Logga contesto qui!
return await response.text()
except aiohttp.ClientError as e:
logging.warning(f"Errore rete: {e}") # Resta interno
# Non rilanciare, poiché è gestione locale
```

Poi in `wrapper`:
```python
try:
result = await fetch_data("http://example.com")
except ServerError as e: # Solo gli errori server vengono qui
print(f"Errore critico: {e}")
# Logga o notifica
```
**Vantaggi**: separazione netta dei livelli, niente più "exception leaking", e il logging contestuale in `fetch_data` (come suggeriva @lucezanella72) ti salverà durante il debugging notturno! Se usi `gather`, aggiungi `return_exceptions=True` per gestire errori multipli senza crash.
Avatar di emiliamancini37
@lucezanella72 ha ragione! Se non logghi il contesto, ti ritrovi a chiederti "ma quel cazzo di errore da dove viene?!" come mi è capitato in un crawler per il lavoro. Però non bastano i custom exception: devi stare attento a **non catturare mai ValueError o eccezioni non specifiche dentro fetch_data**. Io ho risolto definendo una mia classe `class HTTPServerError(Exception): pass` e lanciandola solo dopo check espliciti sugli status code. Così nel wrapper puoi fare:

```python
except HTTPServerError as e:
print(f"Problema 5xx: {e}")
except aiohttp.ClientError as e:
print(f"Errore client: {e}")
```

Ma la vera chicca è **non usare mai `raise` nudo e crudo**! Se vuoi propagare errori specifici, rilancia con `raise HTTPServerError(...) from e` così mantieni la chain originale per debug. E per l'amor del cielo, se fai `asyncio.gather()` senza `return_exceptions=True`, ti ritrovi un aborto totale alla prima 500 - è come buttare via un vassoio di cannoli perché uno è caduto. Metti ordine nelle tue except o poi il debug ti porterà via notti intere.
Avatar di dakotalongo
@emiliamancini37 Hai centrato il punto con il bisturi! Il consiglio su `return_exceptions=True` mi ha evitato un infarto: ora gestisco gli errori singoli senza far crollare tutto. E quel `raise ... from e` è una figata pazzesca, finalmente vedo la chain completa senza impazzire. Avevo il codice pieno di `except Exception` e sembrava un campo minato. Con le tue classi custom tipo `HTTPServerError` adesso è tutto più leggibile, tipo un menu di sushi invece di un cesto di calamari fritti. Grazie, ho applicato tutto e miracolo: il crawler non si blocca più alla prima 500 e i log urlano informazioni utili.
Avatar di elliotriva
@dakotalongo, sono contenta tu abbia risolto! Vedere un crawler che non si inchioda più è una delle soddisfazioni più grandi che ci siano. Anch'io ho passato notti insonni a debuggare codice async pieno di `except Exception`... un incubo!

L'analogia del sushi e del cesto di calamari fritti mi ha fatto morire dal ridere, rende perfettamente l'idea del caos che si può creare senza una buona gestione delle eccezioni.

Un consiglio extra: se usi librerie esterne che potrebbero sollevare eccezioni "strane", valuta di creare una tua eccezione base da cui far derivare le altre. In questo modo, puoi intercettare tutti gli errori della libreria in un colpo solo, senza dover specificare ogni singola eccezione. Io lo faccio spesso quando lavoro con `requests`, per esempio, e mi salva un sacco di tempo.

E per la cronaca, decisamente team sushi tutta la vita!
Avatar di jettnegri23
@elliotriva Team sushi? Ecco perché quando il codice crasha resti zen mentre io butto la tastiera dalla finestra pensando ai calamari in frittura! 😂 Il tuo consiglio sulle eccezioni base è oro: meno male che l'ho imparato dopo un episodio traumatico con una libreria che lanciava 42 tipi di errori diversi. Ora ho una superclasse `WebScraperException` che cattura tutto, e quando fallisce stampo "ERRORE: [meme di un panda che piange]" nel log.

Piccola aggiuntina dark: se non aggiungi un timeout alle tue sessioni aiohttp, quelle 500 diventano un buco nero che inghiotte l'event loop. Chiedimi come lo so... *(spoiler: ho bruciato un server di staging)*.

Però sushi > calamari SEMPRE, anche se i calamari sono la metafora perfetta per gli `except Exception` che esplodono a caso 🦑💥
Avatar di shaysorrentino
@jettnegri23 Ah, il mitico server di staging sacrificato sull'altare di aiohttp senza timeout... *[sorrido amaramente]*. Ci siamo passati tutti: quella sensazione quando l'event loop diventa un buco nero che risucchia CPU come se non ci fosse un domani. Mai sottovalutare i timeout, specie con API esterne che ti ghostano peggio di un match Tinder.

Sulla superclasse `WebScraperException`: scelta **perfetta**. Anch'io ho una gerarchia simile con ereditarietà a tema sushi: `NigiriError` (base), `SashimiTimeout` (per le attese), `WasabiCritical` (per i crash che ti bruciano il naso). E sì, loggare meme invece d'errori grezzi è geniale – io uso un gif di Goku che si arrende con "404: Palla di Drago non trovata".

Ma attenzione al lato oscuro: se lanci *troppi* errori custom, rischi il reverse calamaro – il codice diventa illeggibile come un menu in dialetto napoletano dopo 3 spritz.

E per il sushi vs calamari:
- Sushi (eccezioni mirate) = eleganza, controllo, *umami* del codice.
- Calamari (`except Exception`) = caos fritto che esplode in faccia.

P.S. Se non hai già un decoratore per i timeout globali, ti cambio squadra.

La Tua Risposta

💬

Vuoi partecipare alla discussione?

Accedi o registrati per scrivere la tua risposta e unirti alla conversazione!