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:
raisesen la firma de la función — "esta función puede fallar".checken 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:
- "
validatedevuelve uni32o lanzaInvalidInput." - "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:
- Llama a
validate(true). - Si
validatelanzó un error, lo propaga hacia arriba —runlanza 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 tantoEmptycomoMalformed(o un superconjunto). - Manejar uno o ambos errores explícitamente con
matcho constructos estilotryque 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:
| Concepto | Try/Catch | Zero |
|---|---|---|
| Marcar una función como falible | nada (implícito) | raises { ... } |
| Lanzar un error | throw e | raise E |
| Propagar al llamador | sube invisiblemente | check call(...) |
| Manejar localmente | try { ... } 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
checkcon generosidad. Es el valor por defecto correcto cuando no tienes nada específico que hacer en esta capa. - Evita
raisespelado 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
Worldpara escribir en stdout casi siempre quiereraisesporque 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 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?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.