Ir al contenido
learn

3. Errores: ?, ! y or { }

Código: examples/l03_errors.v y examples/l03_pages.v — v run examples/l03_pages.v desde code/v-for-csharp-java.

V no tiene null ni excepciones. Una función que puede no tener valor devuelve un option, ?T; una función que puede fallar devuelve un result, !T. Ambos forman parte del tipo de retorno, y el compilador se niega a dejarte usar el valor sin decidir qué pasa en el otro caso.

Situación C# Java V
Un valor puede estar ausente null, tipos que aceptan valores null (advertencias) null, Optional<T> ?T, return none
Una operación puede fallar excepciones, todas no comprobadas excepciones comprobadas y no comprobadas !T, return error('…')
Gestionarlo ??, try/catch orElse, try/catch or { }, if x := f() { }
Pasarlo al llamador nada que escribir throws en la firma ! o ? después de la llamada
Un bug, el programa se detiene excepción no gestionada, Environment.FailFast excepción no gestionada, Error panic()

Fuente: Option/Result types and error handling.

const courses = ['duckdb', 'ladybugdb', 'rust-for-csharp-java', 'v-for-csharp-java']
fn find_course(prefix string) ?string {
for c in courses {
if c.starts_with(prefix) {
return c
}
}
return none
}

return c devuelve un string: V lo envuelve en el option. El llamador no puede usar el resultado directamente como un string:

fn main() {
name := find_course('rust')
println(name.to_upper())
}
e03_option_in_expression.v:14:10: error: Option type `string` cannot be called directly, you should unwrap it first
12 | fn main() {
13 | name := find_course('rust')
14 | println(name.to_upper())
| ~~~~
15 | }
e03_option_in_expression.v:14:2: error: `println` can not print void expressions
12 | fn main() {
13 | name := find_course('rust')
14 | println(name.to_upper())
| ~~~~~~~~~~~~~~~~~~~~~~~~
15 | }

C# con tipos de referencia que aceptan valores null solo daría una advertencia (CS8602), y Java compilaría y lanzaría una NullPointerException en tiempo de ejecución. Dos formas de desenvolver:

// or { } da un valor por defecto, como ?? en C# u orElse en Java
println(find_course('lady') or { 'no course' })
println(find_course('zig') or { 'no course' })
// if con desenvolvimiento: la variable solo existe cuando hay un valor
if c := find_course('rust') {
println('found ${c}')
}
// En un bloque or, `err` es el error; para un option es `none`
find_course('java') or { println('java: ${err}') }
ladybugdb
no course
found rust-for-csharp-java
java: none

strconv.atoi devuelve !int: un int, o un error allí donde int.Parse lanzaría una FormatException e Integer.parseInt una NumberFormatException. Un result se debe gestionar en cada llamada:

import strconv
fn main() {
lines := strconv.atoi('474')
println(lines)
}
e03_unhandled_result.v:4:19: error: strconv.atoi() returns `!int`, so it should have either an `or {}` block, or `!` at the end
2 |
3 | fn main() {
4 | lines := strconv.atoi('474')
| ~~~~~~~~~~~
5 | println(lines)
6 | }

Incluso una función que no devuelve nada pero puede fallar, ! a secas, no se puede llamar y olvidar. os.write_file es una de ellas:

import os
fn main() {
os.write_file('journal.md', '## 2026-09-14')
println('saved')
}
e03_unhandled_void.v:4:5: error: os.write_file() returns `!void`, so it should have either an `or {}` block, or `!` at the end
2 |
3 | fn main() {
4 | os.write_file('journal.md', '## 2026-09-14')
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 | println('saved')
6 | }

Esto es más estricto que las excepciones comprobadas de Java, que solo se aplican a las excepciones que declara un método: en V, cada llamada que puede fallar lo dice.

// 1. ! pasa el error al llamador, que a su vez debe devolver un result
fn parse_lines(field string) !int {
n := strconv.atoi(field)!
if n < 0 {
return error('negative line count: ${n}')
}
return n
}
fn total_lines(fields []string) !int {
mut total := 0
for f in fields {
total += parse_lines(f)!
}
return total
}
// 2. Un valor por defecto
println(parse_lines('474') or { -1 })
// 3. Hacer algo con `err`
parse_lines('12abc') or { println(err) }
parse_lines('-3') or { println(err) }
// 4. Salir del bucle, o de la función, desde el bloque or
for fields in [['474', '341'], ['474', 'x', '341']] {
total := total_lines(fields) or {
println('${fields}: ${err}')
continue
}
println('${fields}: ${total} lines')
}
474
strconv.atoi: parsing "12abc": invalid radix 10 character
negative line count: -3
['474', '341']: 815 lines
['474', 'x', '341']: strconv.atoi: parsing "x": invalid radix 10 character

if x := f() { } else { } también funciona con los results, con err en la rama else. error() crea un error a partir de un mensaje; error_with_code() añade un número.

! después de una llamada es el equivalente visible de dejar que una excepción se propague. Necesita una función que devuelva un result:

import strconv
fn parse_lines(field string) int {
return strconv.atoi(field)!
}
e03_propagate_without_result.v:4:28: error: to propagate the Result call, `parse_lines` must return a Result
2 |
3 | fn parse_lines(field string) int {
4 | return strconv.atoi(field)!
| ^
5 | }
6 |
Details: e03_propagate_without_result.v:3:30: details: prepend ! before the declaration of the return type of `parse_lines`
1 | import strconv
2 |
3 | fn parse_lines(field string) int {
| ~~~
4 | return strconv.atoi(field)!
5 | }

Para un option, el mismo operador es ?: dot := title.index('. ')? devuelve none desde la función actual cuando no hay coincidencia.

Un panic detiene el programa: imprime V panic: y un mensaje, luego un backtrace, y termina con el código 1. No hay catch.

Un índice de array fuera de rango provoca un panic, como una IndexOutOfRangeException que nada captura:

fn main() {
courses := ['duckdb', 'ladybugdb']
i := 2
println(courses[i])
}
V panic: array.get: index out of range (i,a.len):2, 2

or { } después de un índice convierte el panic en un valor: courses[10] or { 'index out of range' }.

! en main, que no puede pasar el error más arriba, provoca un panic:

import strconv
fn main() {
lines := strconv.atoi('forty-two')!
println(lines)
}
V panic: result not set (strconv.atoi: parsing "forty-two": invalid radix 10 character)

or { panic(err) } hace lo mismo con el propio mensaje del error:

import os
fn main() {
text := os.read_file('missing.csv') or { panic(err) }
println(text)
}
V panic: failed to open file "missing.csv"; code: 2

La primera línea es la misma en los tres SO. En Windows, las líneas siguientes dan el commit de V, los identificadores del proceso y del hilo, y un backtrace en el archivo C generado, como C://Users//spare//AppData//Local//Temp//v_0//P03_OR~1.C:4689: at builtin___v_panic: Backtrace; su contenido depende del SO y del compilador C, y la CI del curso solo compara la primera línea.

Un error es cualquier tipo que implemente la interfaz IError, con msg() string y code() int. Incrustar el struct integrado Error proporciona ambos, y sobrescribes lo que necesites:

struct ParseError {
Error
line_no int
reason string
}
fn (e ParseError) msg() string {
return 'line ${e.line_no}: ${e.reason}'
}
fn (e ParseError) code() int {
return e.line_no
}

data/pages.csv tiene una línea por página de este sitio. Un primer analizador divide cada línea por las comas, y devuelve un ParseError cuando una línea no tiene cinco campos:

fn parse_page(line_no int, line string) !Page {
fields := line.split(',')
if fields.len != 5 {
return ParseError{
line_no: line_no
reason: 'expected 5 fields, got ${fields.len}'
}
}
lines := strconv.atoi(fields[4]) or {
return ParseError{
line_no: line_no
reason: err.msg()
}
}
return Page{fields[0], fields[1], fields[2], fields[3], lines}
}

return ParseError{…} en una función que devuelve !Page es un error, no una página: V convierte el struct en IError. El llamador reúne los errores y luego comprueba su tipo con is, el equivalente de catch (ParseError e):

lines := os.read_lines('data/pages.csv')!
mut pages := []Page{}
mut errors := []IError{}
for i, line in lines[1..] {
page := parse_page(i + 2, line) or {
errors << err
continue
}
pages << page
}
println('naive split: ${pages.len} pages, ${errors.len} errors')
for e in errors[..3] {
if e is ParseError {
println(' code ${e.code()}: ${e.msg()}')
}
}
naive split: 269 pages, 50 errors
code 10: line 10: expected 5 fields, got 6
code 21: line 21: expected 5 fields, got 6
code 26: line 26: expected 5 fields, got 6

Dentro de if e is ParseError, e es un ParseError con su campo line_no: V hace la conversión automáticamente, como un patrón if (e is ParseError pe) en C#. La línea 10 del archivo muestra por qué falla el analizador ingenuo:

/duckdb/08-persistence/,en,duckdb,"8. Persistence, transactions and concurrency",394

El título va entre comillas porque contiene comas. encoding.csv gestiona las comillas; su lector devuelve ![]string por cada fila, y un error al final del archivo:

mut reader := csv.new_reader(os.read_file('data/pages.csv')!)
reader.read()! // la cabecera
mut parsed := []Page{}
for {
row := reader.read() or {
if err is csv.EndOfFileError {
break
}
panic(err)
}
parsed << Page{row[0], row[1], row[2], row[3], strconv.atoi(row[4])!}
}
encoding.csv: 319 pages
8. Persistence, transactions and concurrency: 394 lines

Una trampa con un error convertido por smart cast: ${err} imprime entonces el struct con todos sus campos, no msg(), así que llama a err.msg() explícitamente (ver el diario).

csv.EndOfFileError se declara sin pub en reader.v, líneas 25-31, y sin embargo V 0.5.2 lo acepta después de is en otro módulo. Llegar al final del archivo se notifica como un error, como io.EOF en Go, no como un option.

  • ?T es un valor o none; !T es un valor o un error. El compilador rechaza una llamada cuyo option o result no se gestiona, incluso para una función que no devuelve nada.
  • or { } da un valor por defecto, o sale con return, continue, break o panic; if x := f() { } desenvuelve; ! y ? pasan el error o el none al llamador.
  • string.int() y sus hermanos nunca fallan: devuelven 0 o saturan. Analiza las entradas no fiables con strconv.
  • Un panic detiene el programa con el código 1, y no se puede capturar. Un índice fuera de rango provoca un panic salvo que tenga or { }.
  • Un error personalizado incrusta Error, sobrescribe msg() y code(), y se reconoce con is.
  1. Escribe fn lesson_number(title string) ?int, que devuelva el número antes de '. ' en un título como '8. Persistence, transactions and concurrency', y none para 'Mission' o 'v2. Draft'. string.index devuelve ?int.
Solución

examples/s03_lesson_number.v:

fn lesson_number(title string) ?int {
dot := title.index('. ')?
return strconv.atoi(title[..dot]) or { return none }
}
8: 8. Persistence, transactions and concurrency
-: Mission
-: v2. Draft
12: 12. Cross-compilation

? transmite el none de index. strconv.atoi devuelve un result, no un option: or { return none } convierte su error en none, y 'v2' da none.

  1. Modifica total_lines para que su error diga qué campo es incorrecto: field 2: strconv.atoi: parsing "x": invalid radix 10 character.
Solución

examples/s03_lesson_number.v:

fn total_lines(fields []string) !int {
mut total := 0
for i, f in fields {
total += strconv.atoi(f) or { return error('field ${i + 1}: ${err.msg()}') }
}
return total
}
815
field 2: strconv.atoi: parsing "x": invalid radix 10 character

return en un bloque or sale de la función con un nuevo error, que envuelve el mensaje del original: el equivalente de lanzar una nueva excepción con la original como excepción interna, sin la cadena.

  1. Un colega lee la columna lines con row[4].int(), y una línea dice n/a. ¿Qué pasa en V, y qué pasaría con int.Parse en C# y con Integer.parseInt en Java?
Solución

En V, 'n/a'.int() devuelve 0: la página cuenta cero líneas y nada informa del problema. int.Parse("n/a") lanza una FormatException, e Integer.parseInt("n/a") una NumberFormatException. strconv.atoi(row[4]) restablece un error que hay que gestionar. La lección 4 sigue usando row[4].int() sobre pages.csv, un archivo cuya columna lines ha comprobado el analizador de esta lección.