3. Erreurs : ?, ! et or { }
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Code : examples/l03_errors.v et examples/l03_pages.v — v run examples/l03_pages.v depuis code/v-for-csharp-java.
Deux choses que V n’a pas
Section intitulée « Deux choses que V n’a pas »V n’a ni null ni exceptions. Une fonction qui peut ne pas avoir de valeur renvoie une option, ?T ; une fonction qui peut échouer renvoie un result, !T. Les deux font partie du type de retour, et le compilateur refuse que tu utilises la valeur sans décider de ce qui se passe dans l’autre cas.
| Situation | C# | Java | V |
|---|---|---|---|
| Une valeur peut être absente | null, types référence nullables (avertissements) |
null, Optional<T> |
?T, return none |
| Une opération peut échouer | exceptions, toutes non vérifiées | exceptions vérifiées et non vérifiées | !T, return error('…') |
| La traiter | ??, try/catch |
orElse, try/catch |
or { }, if x := f() { } |
| La transmettre à l’appelant | rien à écrire | throws dans la signature |
! ou ? après l’appel |
| Un bug, le programme s’arrête | exception non gérée, Environment.FailFast |
exception non gérée, Error |
panic() |
Source : Option/Result types and error handling.
Options : ?T et none
Section intitulée « Options : ?T et none »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 renvoie une string : V l’enveloppe dans l’option. L’appelant ne peut pas utiliser le résultat directement comme une 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# avec les types référence nullables ne ferait qu’avertir (CS8602), et Java compilerait puis lèverait une NullPointerException à l’exécution. Deux façons de déballer :
// or { } donne une valeur par défaut, comme ?? en C# ou orElse en Javaprintln(find_course('lady') or { 'no course' })println(find_course('zig') or { 'no course' })
// if unwrapping : la variable n'existe que s'il y a une valeurif c := find_course('rust') { println('found ${c}')}
// Dans un bloc or, `err` est l'erreur ; pour une option, c'est `none`find_course('java') or { println('java: ${err}') }ladybugdbno coursefound rust-for-csharp-javajava: noneResults : !T et error()
Section intitulée « Results : !T et error() »strconv.atoi renvoie !int : un int, ou une erreur là où int.Parse lèverait une FormatException et Integer.parseInt une NumberFormatException. Un result doit être traité à chaque appel :
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 | }Même une fonction qui ne renvoie rien mais peut échouer, ! seul, ne peut pas être appelée puis oubliée. os.write_file en est une :
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 | }C’est plus strict que les exceptions vérifiées de Java, qui ne s’appliquent qu’aux exceptions qu’une méthode déclare : en V, chaque appel qui peut échouer le dit.
Quatre façons de traiter un result
Section intitulée « Quatre façons de traiter un result »// 1. ! transmet l'erreur à l'appelant, qui doit lui-même renvoyer un resultfn 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. Une valeur par défautprintln(parse_lines('474') or { -1 })// 3. Faire quelque chose avec `err`parse_lines('12abc') or { println(err) }parse_lines('-3') or { println(err) }// 4. Quitter la boucle, ou la fonction, depuis le bloc orfor fields in [['474', '341'], ['474', 'x', '341']] { total := total_lines(fields) or { println('${fields}: ${err}') continue } println('${fields}: ${total} lines')}474strconv.atoi: parsing "12abc": invalid radix 10 characternegative line count: -3['474', '341']: 815 lines['474', 'x', '341']: strconv.atoi: parsing "x": invalid radix 10 characterif x := f() { } else { } fonctionne aussi pour les results, avec err dans la branche else. error() crée une erreur à partir d’un message ; error_with_code() ajoute un numéro.
! après un appel est l’équivalent visible du fait de laisser une exception se propager. Il lui faut une fonction qui renvoie 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 | }Pour une option, le même opérateur est ? : dot := title.index('. ')? renvoie none depuis la fonction courante quand il n’y a pas de correspondance.
Un panic arrête le programme : il affiche V panic: et un message, puis une backtrace, et se termine avec le code 1. Il n’y a pas de catch.
Un index de tableau hors limites déclenche un panic, comme une IndexOutOfRangeException que rien n’intercepte :
fn main() { courses := ['duckdb', 'ladybugdb'] i := 2 println(courses[i])}V panic: array.get: index out of range (i,a.len):2, 2or { } après un index transforme le panic en valeur : courses[10] or { 'index out of range' }.
! dans main, qui ne peut pas transmettre l’erreur plus loin, déclenche 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) } fait la même chose avec le message propre à l’erreur :
import os
fn main() { text := os.read_file('missing.csv') or { panic(err) } println(text)}V panic: failed to open file "missing.csv"; code: 2La première ligne est la même sur les trois OS. Sous Windows, les lignes suivantes donnent le commit de V, les identifiants du processus et du thread, et une backtrace dans le fichier C généré, comme C://Users//spare//AppData//Local//Temp//v_0//P03_OR~1.C:4689: at builtin___v_panic: Backtrace ; son contenu dépend de l’OS et du compilateur C, et la CI du cours ne compare que la première ligne.
Types d’erreur personnalisés
Section intitulée « Types d’erreur personnalisés »Une erreur est n’importe quel type qui implémente l’interface IError, avec msg() string et code() int. Embarquer la struct intégrée Error fournit les deux, et tu redéfinis ce dont tu as besoin :
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 contient une ligne par page de ce site. Un premier parseur découpe chaque ligne sur les virgules, et renvoie une ParseError quand une ligne n’a pas cinq champs :
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{…} dans une fonction qui renvoie !Page est une erreur, pas une page : V convertit la struct en IError. L’appelant collecte les erreurs, puis teste leur type avec is, l’équivalent 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 6Dans if e is ParseError, e est une ParseError avec son champ line_no : V la convertit automatiquement, comme un motif if (e is ParseError pe) en C#. La ligne 10 du fichier montre pourquoi le parseur naïf échoue :
/duckdb/08-persistence/,en,duckdb,"8. Persistence, transactions and concurrency",394Le titre est entre guillemets parce qu’il contient des virgules. encoding.csv gère les guillemets ; son lecteur renvoie ![]string pour chaque ligne, et une erreur à la fin du fichier :
mut reader := csv.new_reader(os.read_file('data/pages.csv')!)reader.read()! // l'en-têtemut 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 linesUn piège avec une erreur ayant subi un smart cast : ${err} affiche alors la struct avec tous ses champs, pas msg(), donc appelle err.msg() explicitement (voir le journal).
csv.EndOfFileError est déclarée sans pub dans reader.v, lignes 25-31, et pourtant V 0.5.2 l’accepte après is dans un autre module. Atteindre la fin du fichier est signalé comme une erreur, comme io.EOF en Go, et non comme une option.
À retenir
Section intitulée « À retenir »?Test une valeur ounone;!Test une valeur ou une erreur. Le compilateur refuse un appel dont l’option ou le result n’est pas traité, même pour une fonction qui ne renvoie rien.or { }donne une valeur par défaut, ou sort avecreturn,continue,breakoupanic;if x := f() { }déballe ;!et?transmettent l’erreur ou lenoneà l’appelant.string.int()et ses semblables n’échouent jamais : ils renvoient 0 ou saturent. Analyse les entrées non fiables avecstrconv.- Un panic arrête le programme avec le code 1, et ne peut pas être intercepté. Un index hors limites déclenche un panic, sauf s’il a un
or { }. - Une erreur personnalisée embarque
Error, redéfinitmsg()etcode(), et se reconnaît avecis.
Exercices
Section intitulée « Exercices »- Écris
fn lesson_number(title string) ?int, qui renvoie le nombre avant'. 'dans un titre comme'8. Persistence, transactions and concurrency', etnonepour'Mission'ou'v2. Draft'.string.indexrenvoie?int.
Solution
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. Draft12: 12. Cross-compilation? transmet le none de index. strconv.atoi renvoie un result, pas une option : or { return none } transforme son erreur en none, et 'v2' donne none.
- Modifie
total_linespour que son erreur indique quel champ est erroné :field 2: strconv.atoi: parsing "x": invalid radix 10 character.
Solution
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}815field 2: strconv.atoi: parsing "x": invalid radix 10 characterreturn dans un bloc or quitte la fonction avec une nouvelle erreur, qui reprend le message de l’erreur d’origine : l’équivalent de lever une nouvelle exception avec l’originale comme exception interne, sans la chaîne.
- Un collègue lit la colonne
linesavecrow[4].int(), et une ligne contientn/a. Que se passe-t-il en V, et que se passerait-il avecint.Parseen C# etInteger.parseInten Java ?
Solution
En V, 'n/a'.int() renvoie 0 : la page compte zéro ligne et rien ne signale le problème. int.Parse("n/a") lève une FormatException, et Integer.parseInt("n/a") une NumberFormatException. strconv.atoi(row[4]) rétablit une erreur qu’il faut traiter. La leçon 4 utilise encore row[4].int() sur pages.csv, un fichier dont le parseur de cette leçon a vérifié la colonne lines.
- Documentation de V — Option/Result types and error handling, Custom error types, If unwrapping
- Bibliothèque standard de V —
builtin(IError,error,panic),strconv,encoding.csv - C# — Exceptions et gestion des exceptions, Types référence nullables
- Java — Exceptions vérifiées et non vérifiées,
Optional