8.6. Excepciones

Las excepciones dentro de un script asyncio se comportan casi igual que en Python normal – se propagan hacia arriba por la cadena de llamadas hasta que algo las captura. Casi porque las tareas se ejecutan en paralelo, así que el camino «hacia arriba» no es el camino que creó la tarea. Esta página cubre adónde van las excepciones en cada una de las formas comunes.

8.6.1. Dentro de una corrutina

Un try/except dentro de una corrutina captura las excepciones lanzadas por cualquier cosa a la que haga await, de la manera habitual:

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

Aquí no hay nada específico de asyncio – la cláusula except ve las excepciones lanzadas dentro de fetch como si hubiera sido una llamada a función normal.

8.6.2. En una tarea que la aplicación está esperando

Cuando una corrutina que se ejecuta como una Task lanza una excepción, esta se almacena en la tarea. La próxima vez que algo haga await de esa tarea, la excepción se vuelve a lanzar en el await:

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

Lo mismo aplica a asyncio.gather(). El comportamiento predeterminado – un hijo lanza una excepción, los demás se cancelan, la excepción se propaga fuera del gather – proviene de este mecanismo.

8.6.3. En una tarea que nadie espera

Una tarea que nadie llega a esperar con await es el caso que necesita atención. La excepción sigue ocurriendo; el bucle nota que la tarea terminó con una excepción no manejada; pero no hay ningún await en el que aflorar. El comportamiento predeterminado es imprimir un traceback a través de sys.stderr y seguir ejecutándose – lo cual está bien para un diagnóstico desatendido, pero encaja mal con una aplicación que quería enterarse.

La solución correcta suele ser esperar la tarea. Ya sea directamente, recordando el manejador y esperándolo durante el apagado, o de forma implícita a través de gather() o wait_for(). El patrón de «apagar una aplicación» de la página Tiempos de espera y cancelación cubre este caso para las tareas en segundo plano de larga duración que un script típico genera.

8.6.4. Manejador de excepciones personalizado

Cuando el comportamiento silencioso de traceback-y-continuar no es suficiente, el bucle expone un enganche – Loop.set_exception_handler – que la aplicación puede sobrescribir para hacer otra cosa:

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)

El argumento context es un diccionario con las claves 'message', 'exception' y 'future'. La excepción puede faltar en ciertos eventos de tipo advertencia, razón por la cual el ejemplo usa .get().

Los usos típicos son registrar el fallo en la memoria flash, parpadear un LED de error o escalar a un reinicio por watchdog. La página de control del bucle cubre toda la superficie de los enganches del bucle.

8.6.5. KeyboardInterrupt

Cuando un script se detiene desde el exterior – normalmente porque el IDE le pide que se detenga – la solicitud llega dentro del script como un KeyboardInterrupt. Dentro de asyncio.run() se propaga como lo haría cualquier otra excepción no manejada: main se cancela, todas las tareas que el bucle está rastreando también se cancelan, y el KeyboardInterrupt se vuelve a lanzar fuera de asyncio.run(). Las cláusulas finally se ejecutan al salir, de modo que el mismo patrón de limpieza de la página de cancelación es lo que lo gestiona.