Menu

Raises y Check en Zero: fallos explícitos sin excepciones

Las funciones de Zero declaran sus modos de fallo con raises y quienes las invocan los reconocen con check. Cómo funciona el sistema, por qué no hay throw silencioso y cómo interactúa con la capacidad World.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

El planteamiento

El fallo en Zero no es un flujo de control aparte y paralelo. Es parte del flujo de control regular, declarado en la firma de una función y reconocido en cada sitio de llamada. Dos piezas hacen que esto funcione:

  • raises en la firma de la función — "esta función puede fallar".
  • check en el sitio de llamada — "si esto falla, haz que mi función falle con el mismo error".

La combinación basta para expresar lo que la mayoría de lenguajes resuelven con try/catch o tipos Result.

Declarar una función falible

Añade raises tras el tipo de retorno:

fun validate(ok: Bool) -> i32 raises { InvalidInput } {
    if ok == false {
        raise InvalidInput
    }
    return 42
}

Dos formas de leer la firma:

  • "validate devuelve un i32 o lanza InvalidInput."
  • "El conjunto de resultados posibles es { i32, InvalidInput }."

Ambas lecturas son correctas. El compilador rastrea ambas posibilidades y exige que los llamadores hagan algo explícito con cada una.

También puedes escribir una cláusula raises pelada:

pub fun main(world: World) -> Void raises {
    check world.out.write("hola\n")
}

raises (sin lista de errores) significa "esta función puede fallar con cualquier error". En main esa es la forma convencional: el programa puede terminar con un estado distinto de cero si algo sale mal, y el runtime se encarga de hacer aflorar el error.

Para funciones más profundas en la pila de llamadas, prefiere la forma explícita raises { ErrorA, ErrorB } para que los modos de fallo queden documentados a cada nivel.

Lanzar un error

Dentro de una función falible, raise termina la función con el error dado:

fun validate(ok: Bool) -> i32 raises { InvalidInput } {
    if ok == false {
        raise InvalidInput
    }
    return 42
}

raise InvalidInput produce un error InvalidInput en esa línea. La función no continúa más allá de ese punto — el control vuelve al llamador, y el llamador ve el error en lugar de un i32. La cláusula raises lista los únicos tipos de error que esta función puede lanzar; lanzar algo fuera de la lista es un error de compilación.

Propagar un error con check

Un llamador que invoca una función falible tiene que reconocer el posible fallo. El reconocimiento más común es check:

fun run() -> Void raises { InvalidInput } {
    check validate(true)
}

check validate(true) hace dos cosas:

  1. Llama a validate(true).
  2. Si validate lanzó un error, lo propaga hacia arriba — run lanza el mismo error a su llamador.

Para que se permita la propagación, run tiene que declarar que puede lanzar InvalidInput (o algo compatible) en su propia cláusula raises. El compilador lo comprueba. Si la firma de run dijera raises { OtherError }, la propagación no compilaría porque InvalidInput no está en el conjunto.

Un ejemplo completo trabajado de los samples oficiales del lenguaje — pulsa Ejecutar para ver la propagación con éxito:

El tipo de error viaja con la firma de la función hasta arriba en la pila de llamadas. El raises pelado de main acepta cualquier cosa que run pueda lanzar, así que la propagación aterriza limpiamente.

¿Por qué no try/catch?

La disciplina de diseño detrás de raises/check es que el fallo nunca es invisible en un sitio de llamada. En un lenguaje con try/catch, una excepción puede atravesar silenciosamente una función que ni sabe que está implicada — la función parece pura, pero una llamada profunda en su cuerpo lanza y la excepción se desenrolla por encima.

Eso es cómodo para el autor del código que lanza. Es caro para todos los demás:

  • Lectores (y agentes) no pueden saber por la firma si una función participa en rutas de fallo.
  • Refactorizar se vuelve nervioso — mover una llamada entre funciones puede cambiar qué excepciones son alcanzables.
  • El código de recuperación vive lejos del sitio que sabe qué hacer.

Zero paga el coste por adelantado — anotaciones en cada función falible y check en cada llamada falible — para obtener la propiedad "los modos de fallo en los que participa una función son visibles desde su firma". Esa es una propiedad en la que humanos y agentes pueden confiar.

¿Por qué no simplemente Result<T, E>?

Podrías expresar lo mismo con un choice — un tipo Result<T, E> con variantes ok y err. Zero te ofrece también ese patrón; es una herramienta útil cuando el fallo es datos que quieres inspeccionar, guardar o pasar por ahí.

Lo que raises/check añade es una convención a nivel de sintaxis para el caso común: "si esto falla, haz que mi función falle de la misma manera". Sin ella, cada llamada tendría que envolverse en un match que casi siempre re-empaqueta el error en el propio Result del llamador. check es el atajo para eso, con el compilador garantizando que la propagación está bien tipada.

Por tanto:

  • Result<T, E> (un choice) — cuando quieres inspeccionar o transportar el fallo como un valor.
  • raises + check — cuando solo quieres propagar el fallo por la pila de llamadas.

Ambos están disponibles; cubren necesidades ergonómicas distintas.

Varios tipos de error

Una función puede lanzar más de un tipo de error:

fun parse(input: String) -> i32 raises { Empty, Malformed } {
    if std.mem.len(input) == 0 {
        raise Empty
    }
    // ... lógica de parseo ...
    raise Malformed
}

Un llamador puede:

  • Propagar con check parse(input) si su propia firma lista tanto Empty como Malformed (o un superconjunto).
  • Manejar uno o ambos errores explícitamente con match o constructos estilo try que el lenguaje exponga para el propósito.

La sintaxis exacta para el manejo fino (hacer match sobre tipos de error específicos vs. propagar los demás) es una de las superficies que pueden moverse en Zero pre-1.0. El contrato — cada tipo de error que una función puede lanzar está en su firma — es la parte estable.

Un modelo mental

El sistema de fallos de Zero es una versión estricta-honesta de lo que tiene cualquier lenguaje imperativo:

ConceptoTry/CatchZero
Marcar una función como faliblenada (implícito)raises { ... }
Lanzar un errorthrow eraise E
Propagar al llamadorsube invisiblementecheck call(...)
Manejar localmentetry { ... } catch(e) { ... }match sobre un Result devuelto, o una forma de manejo tipada

La diferencia de comportamiento es pequeña. La diferencia anotacional es grande, y a propósito. Los efectos son explícitos. Los fallos son efectos.

Notas de estilo

  • Usa check con generosidad. Es el valor por defecto correcto cuando no tienes nada específico que hacer en esta capa.
  • Evita raises pelado en helpers internos. Cuanto más estrecho el conjunto de errores, más útil la firma.
  • Empareja las operaciones falibles con las capacidades que necesitan. Una función que usa World para escribir en stdout casi siempre quiere raises porque las escrituras pueden fallar.

Lo siguiente: diagnósticos JSON

raises/check trabaja codo con codo con los diagnósticos del compilador de Zero — cuando escribes check contra una función que no declara los errores adecuados, el compilador te dice exactamente qué pasa en una forma estructurada. La próxima página cubre diagnósticos JSON — la fuente legible por máquinas que lee un agente para reparar código.

Preguntas frecuentes

¿Qué significa raises en Zero?

raises en la firma de una función declara que la función puede fallar. Un raises pelado permite cualquier tipo de error. Una forma específica como raises { InvalidInput } restringe la función a fallar solo con los tipos de error listados. Quienes llaman deben reconocer la posibilidad de fallo, o bien con check, o bien con otra forma explícita de manejo.

¿Qué hace el operador check?

check expr evalúa expr y, si produce un error, lo propaga al llamador de la función actual. Piénsalo como 'ejecuta esto y, si falla, haz que mi llamador falle con el mismo error'. Para que se permita la propagación, la cláusula raises del llamador debe a su vez declarar que puede lanzar un error compatible.

¿Cómo se lanza un error en Zero?

Usa raise ErrorName dentro de una función cuya cláusula raises incluya ese error. Ejemplo: if ok == false { raise InvalidInput }. La función termina en ese punto; el error se convierte en el resultado de la función, sobre el que el llamador puede aplicar check.

¿Por qué Zero no usa try/catch?

Try/catch permite que las excepciones atraviesen silenciosamente funciones que no son conscientes de ellas. El diseño de Zero es que cada firma de función debe reconocer los modos de fallo en los que participa. No hay flujo de control oculto — si una función puede fallar, su firma lo dice, y cada llamador tiene que reconocerlo explícitamente con check.

¿Puede una función lanzar varios tipos de error en Zero?

Sí — lístalos en la cláusula raises { ... }, separados por comas (o siguiendo la sintaxis actual de Zero para conjuntos de errores). El conjunto de errores posibles es parte del contrato de la función, igual que sus tipos de parámetros y retorno. Los llamadores pueden hacer pattern matching sobre qué error se lanzó o simplemente propagar con check.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR