8.6. Exceptions

Les exceptions à l’intérieur d’un script asyncio se comportent presque de la même façon qu’en Python ordinaire – elles remontent la chaîne d’appels jusqu’à ce que quelque chose les attrape. Presque, car les tâches s’exécutent en parallèle, de sorte que le chemin « vers le haut » n’est pas le chemin qui a créé la tâche. Cette page couvre l’endroit où vont les exceptions dans chacune des configurations courantes.

8.6.1. À l’intérieur d’une coroutine

Un try/except à l’intérieur d’une coroutine attrape les exceptions levées par tout ce qu’elle await, de la manière habituelle

async def fetch_with_retry(url):
    for attempt in range(3):
        try:
            return await fetch(url)
        except OSError as e:
            last_error = e
    raise last_error

Rien de spécifique à asyncio ici – la clause except voit les exceptions levées à l’intérieur de fetch comme s’il s’était agi d’un appel de fonction ordinaire.

8.6.2. Dans une tâche que l’application attend

Lorsqu’une coroutine s’exécutant en tant que Task lève une exception, celle-ci est stockée sur la tâche. La prochaine fois que quelque chose await cette tâche, l’exception est de nouveau levée au niveau de l”await

task = asyncio.create_task(may_fail())
try:
    result = await task
except OSError:
    log("may_fail failed")

Il en va de même pour asyncio.gather(). Le comportement par défaut – un enfant lève une exception, les autres sont annulés, l’exception se propage hors du gather – provient de ce mécanisme.

8.6.3. Dans une tâche que personne n’attend

Une tâche que personne n”await jamais est le cas qui requiert de l’attention. L’exception se produit tout de même ; la boucle remarque que la tâche s’est terminée avec une exception non gérée ; mais il n’y a aucun await où elle puisse remonter à la surface. Le comportement par défaut consiste à afficher une trace d’appels via sys.stderr et à continuer de s’exécuter – ce qui convient pour un diagnostic sans surveillance, mais convient mal à une application qui voulait être au courant.

La bonne solution consiste généralement à attendre la tâche. Soit directement, en mémorisant le handle et en l’attendant lors de l’arrêt, soit implicitement via gather() ou wait_for(). Le motif « arrêter une application » de la page Délais et annulation couvre ce cas pour les tâches d’arrière-plan de longue durée qu’un script typique engendre.

8.6.4. Gestionnaire d’exceptions personnalisé

Lorsque la trace d’appels silencieuse suivie d’une poursuite ne suffit pas, la boucle expose un point d’accroche – Loop.set_exception_handler – que l’application peut remplacer pour faire autre chose

def handler(loop, context):
    print("asyncio:", context.get("message"))
    if "exception" in context:
        sys.print_exception(context["exception"])

loop = asyncio.get_event_loop()
loop.set_exception_handler(handler)

L’argument context est un dictionnaire avec les clés 'message', 'exception' et 'future'. L’exception peut être absente lors de certains événements de type avertissement, ce qui explique pourquoi l’exemple utilise .get().

Les utilisations typiques consistent à consigner la défaillance en mémoire flash, à faire clignoter une LED d’erreur, ou à escalader vers un redémarrage par chien de garde. La page contrôle de la boucle couvre l’ensemble des points d’accroche de la boucle.

8.6.5. KeyboardInterrupt

Lorsqu’un script est arrêté de l’extérieur – généralement parce que l’IDE lui demande de s’arrêter – la requête parvient à l’intérieur du script sous la forme d’un KeyboardInterrupt. À l’intérieur de asyncio.run(), il se propage comme le ferait toute autre exception non gérée : main est annulé, chaque tâche que la boucle suit est annulée elle aussi, et le KeyboardInterrupt est de nouveau levé hors de asyncio.run(). Les clauses finally s’exécutent au passage, de sorte que le même motif de nettoyage de la page sur l’annulation est ce qui le prend en charge.