Manejo de errores en V sin excepciones: Option, Result, or y el operador !

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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP