2.26. Lanzar errores

Una función puede señalar un problema a quien la llama lanzando una excepción. La palabra clave es raise:

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

Llamar a square_root(-1) se detiene en la línea raise, sale de la función y busca un except coincidente en quien la llama. Si nadie la captura, el script termina con un traceback.

2.26.1. Por qué lanzar en lugar de devolver un valor centinela

Dos formas de informar de «entrada incorrecta»:

# 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

La forma con excepción suele ser mejor:

  • Quien llama tiene que manejar el caso de error deliberadamente – ya sea con un try, o dejando que la excepción se propague. Los centinelas son fáciles de olvidar y fáciles de confundir con un resultado normal.

  • El mensaje de error viaja junto con la excepción; el enfoque del centinela tiene que adjuntar el diagnóstico en otro lugar.

  • El comportamiento por defecto ante una excepción no manejada es un fallo estrepitoso con un traceback que apunta a la llamada culpable. Los retornos silenciosos de None se convierten en errores sutiles más adelante.

Recurre a los centinelas solo cuando «no encontrado» es un resultado rutinario y no excepcional – dict.get() devuelve None ante una clave inexistente precisamente porque se espera que las búsquedas a veces fallen.

2.26.2. Clases de excepción personalizadas

Para lanzar un problema que quien llama quizá quiera distinguir de los errores integrados, define una subclase 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)

El cuerpo vacío de la class está bien – lo que importa es el nombre en sí, porque quien llama captura por clase. Agrupa errores relacionados bajo una base común si quien llama pudiera querer capturar toda la familia en un solo bloque.

2.26.2.1. Relanzar

Un raise desnudo dentro de un bloque except relanza la excepción actual de modo que se propague al siguiente manejador:

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

Esta es la forma correcta cuando una función quiere observar un error (registrarlo, contarlo, deshacer un cambio parcial) sin manejarlo realmente.

2.26.3. Cuándo capturar y cuándo propagar

Una regla práctica útil:

  • Captura una excepción en el nivel que pueda recuperarse de forma significativa – sustituir por un valor por defecto, reintentar, omitir la entrada incorrecta.

  • Déjala propagarse cuando no haya nada útil que hacer salvo fallar, o cuando la capa superior sea la que sabe cómo recuperarse.

Una función en medio de una pila de llamadas que se traga todos los errores y devuelve en silencio hace que los fallos sean imposibles de rastrear. Prefiere dejar que las excepciones viajen hasta que alcancen el código que realmente tiene un plan.