V no tiene excepciones. Ni try/catch, ni throw. Todo el manejo de errores se resuelve con dos tipos incorporados en el propio sistema de tipos, Option y Result, y un bloque or que obliga a decidir qué hacer en el sitio exacto donde algo puede fallar. Si ya leíste el artículo anterior de la serie sobre memoria, esto te va a sonar a la misma filosofía: hacer explícito en el tipo lo que en otros lenguajes queda oculto en el control de flujo.
Result: una función que puede fallar
Cuando una función puede devolver un error, se declara con ! antes del tipo de retorno:
fn find_user_by_id(id int, users []User) !User {
for user in users {
if user.id == id {
return user
}
}
return error('usuario ${id} no encontrado')
}
El tipo real de esa función es !User: o devuelve un User, o devuelve un error. No hay un tercer camino oculto, y el compilador te obliga a manejarlo cuando llamas a la función.
El bloque or: la única forma de desempaquetar
user := find_user_by_id(10, users) or {
println(err)
return
}
// con valor por defecto
user := find_user_by_id(10, users) or { User{ name: 'invitado' } }
Dentro del bloque or, la variable err contiene el mensaje que pasaste a error(). Es parecido al if err != nil de Go, con una diferencia clave: en V es sintácticamente imposible ignorar el error sin más, el bloque or es obligatorio salvo que propagues explícitamente con ! o ?.
Propagar el error hacia arriba
Si no quieres manejar el error ahí mismo y prefieres que suba a quien te llamó, añades ! después de la llamada:
fn f(url string) !string {
resp := http.get(url)!
return resp.body
}
// equivale exactamente a:
fn f(url string) !string {
resp := http.get(url) or { return err }
return resp.body
}
Es el mismo patrón que en Go resolvías con if err != nil { return err } repetido tres veces por función, pero condensado a un carácter.
Option: cuando lo que falta no es un error
Para valores que pueden simplemente no existir, sin que eso sea un fallo, V usa ?Type en vez de !Type. La diferencia es de intención: un Option vacío es none, no un error con mensaje. Se maneja igual, con or:
fn find_by_name(name string, users []User) ?User {
for user in users {
if user.name == name {
return user
}
}
return none
}
user := find_by_name('Ana', users) or { return }
Errores personalizados con la interfaz IError
Cuando un simple string no basta, puedes definir tu propio tipo de error implementando la interfaz IError con un método msg():
struct PathError {
Error
path string
}
fn (err PathError) msg() string {
return 'no se pudo abrir la ruta: ${err.path}'
}
fn try_open(path string) ! {
return PathError{ path: path }
}
Esto te permite hacer pattern matching sobre el tipo de error en el bloque or, algo similar a lo que Go resuelve con errors.As y errors.Is desde la 1.13, o a lo que en Rust hacen crates como thiserror.
Cómo se compara con Go y Rust
El diseño de V está a medio camino: toma de Rust la idea de un tipo de retorno que incluye el error (como Result<T, E>), pero sin la ceremonia de definir enums de error propios salvo que quieras ir más allá de un string. Y toma de Go la costumbre de manejar el error en el sitio exacto donde ocurre, sin propagación automática tipo excepción, pero eliminando el boilerplate de if err != nil repetido. Si quieres ver el enfoque de Go con todo detalle, con fmt.Errorf, errors.Is y errors.As, lo tienes en este artículo. Y si prefieres ver cómo Rust resuelve lo mismo con crates dedicados, tenemos esta guía de thiserror y anyhow.
En el próximo artículo de la serie tocamos concurrencia: spawn, canales y el modelo de threads de V.
Imagen: Pexels / Godfrey Atima
