2.26. Lançando erros

Uma função pode sinalizar um problema ao seu chamador lançando uma exceção. A palavra-chave é raise:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

Chamar square_root(-1) para na linha raise, salta para fora da função e procura um except correspondente no chamador. Se nenhum chamador o capturar, o script termina com um traceback.

2.26.1. Por que lançar em vez de retornar um sentinela

Duas formas de relatar “entrada inválida”:

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

A forma com exceção costuma ser melhor:

  • O chamador tem que tratar o caso de erro deliberadamente – seja com um try, seja deixando a exceção propagar. Sentinelas são fáceis de esquecer e fáceis de confundir com um resultado normal.

  • A mensagem de erro viaja junto com a exceção; a abordagem do sentinela tem que anexar o diagnóstico em algum outro lugar.

  • O comportamento padrão para uma exceção não tratada é uma falha estrondosa com um traceback que aponta para a chamada ofensora. Retornos silenciosos de None se tornam bugs sutis mais tarde.

Recorra a sentinelas apenas quando “não encontrado” for um resultado rotineiro e não excepcional – dict.get() retorna None em uma chave ausente justamente porque se espera que buscas às vezes falhem.

2.26.2. Classes de exceção personalizadas

Para lançar um problema que o chamador talvez queira distinguir dos erros embutidos, defina uma subclasse de Exception:

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

Um corpo de class vazio está perfeito – o nome em si é o que importa, porque os chamadores capturam por classe. Agrupe erros relacionados sob uma base comum se um chamador puder querer capturar a família inteira em um único bloco.

2.26.2.1. Relançando

Um raise simples dentro de um bloco except relança a exceção atual para que ela propague ao próximo tratador:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

Esta é a forma correta quando uma função quer observar um erro (registrá-lo, contá-lo, desfazer uma alteração parcial) sem de fato tratá-lo.

2.26.3. Quando capturar e quando propagar

Uma regra prática útil:

  • Capture uma exceção no nível que possa se recuperar de forma significativa – substituir por um padrão, tentar novamente, pular a entrada inválida.

  • Deixe-a propagar quando não houver nada útil a fazer além de falhar, ou quando a camada acima for a que sabe como se recuperar.

Uma função no meio de uma pilha de chamadas que engole todo erro e retorna silenciosamente torna as falhas impossíveis de rastrear. Prefira deixar as exceções viajarem até alcançarem um código que realmente tenha um plano.