8.6. Exceções

As exceções dentro de um script asyncio comportam-se quase da mesma forma que em Python normal – propagam-se pela cadeia de chamadas até que algo as capture. Quase, porque as tarefas estão a correr em paralelo, pelo que o caminho «acima» não é o caminho que criou a tarefa. Esta página aborda para onde vão as exceções em cada uma das formas comuns.

8.6.1. Dentro de uma corrotina

Um try/except dentro de uma corrotina captura exceções lançadas por qualquer coisa que ela awaite, da forma 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

Nada específico do asyncio aqui – a cláusula except vê as exceções lançadas dentro de fetch como se tivesse sido uma chamada de função normal.

8.6.2. Numa tarefa que a aplicação está a aguardar

Quando uma corrotina que está a correr como Task lança uma exceção, esta é armazenada na tarefa. Da próxima vez que algo fizer awaita essa tarefa, a exceção é relançada no await

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

O mesmo se aplica a asyncio.gather(). O comportamento padrão – um filho lança, os outros são cancelados, a exceção propaga-se para fora do gather – resulta deste mecanismo.

8.6.3. Numa tarefa que ninguém aguarda

Uma tarefa que nunca é awaitada é o caso que requer atenção. A exceção ainda acontece; o ciclo repara que a tarefa terminou com uma exceção não tratada; mas não há nenhum await onde ela possa surgir. O comportamento padrão é imprimir um traceback através de sys.stderr e continuar a executar – o que é aceitável para um diagnóstico não acompanhado, mas inadequado para uma aplicação que queria saber.

A correção certa é normalmente aguardar a tarefa. Diretamente, lembrando o identificador e aguardando-o durante o encerramento, ou implicitamente através de gather() ou wait_for(). O padrão «encerrar uma aplicação» da página Timeouts e cancelamento cobre este caso para as tarefas de segundo plano de longa duração que um script típico cria.

8.6.4. Gestor de exceções personalizado

Quando traceback-e-continuar silencioso não é suficiente, o ciclo expõe um gancho – Loop.set_exception_handler – que a aplicação pode substituir para fazer outra coisa:

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)

O argumento context é um dict com as chaves 'message', 'exception', e 'future'. A exceção pode estar ausente em certos eventos do tipo aviso, razão pela qual o exemplo usa .get().

Os usos típicos são registar a falha em flash, fazer piscar um LED de erro, ou escalar para um reboot por watchdog. A página de controlo do ciclo cobre a superfície completa dos ganchos do ciclo.

8.6.5. KeyboardInterrupt

Quando um script é interrompido do exterior – normalmente pelo IDE a pedir-lhe que pare – o pedido chega dentro do script como um KeyboardInterrupt. Dentro de asyncio.run(), propaga-se como qualquer outra exceção não tratada: main é cancelado, cada tarefa que o ciclo está a rastrear também é cancelada, e o KeyboardInterrupt é relançado para fora de asyncio.run(). As cláusulas finally executam no caminho de saída, pelo que o mesmo padrão de limpeza da página de cancelamento é o que o trata.