エラーハンドリング
NagiScript のエラーハンドリング戦略を学びます。
Result 型
NagiScript では Result<T, E> 型を使用してエラーを表現します。
error.ngs
enum Result<T, E> {
Ok(T)
Err(E)
}
基本的な使い方
関数の戻り値として
divide.ngs
fn divide(a: f64, b: f64) Result<f64, str> {
if b == 0.0 {
Result.Err("ZeroDivision")
} else {
Result.Ok(a / b)
}
}
fn main() void {
val result = divide(10.0, 3.0)
match result {
Result.Ok(v) => io.println("Result: " + str(v)),
Result.Err(e) => io.println("Error: " + e)
}
}
? 演算子
エラーを上位関数に伝播させる ? 演算子:
process.ngs
fn parse_int(s: str) Result<i32, str> {
// 実際のパース処理
if s.len() == 0 {
Result.Err("EmptyString")
} else {
// 簡易的な実装
Result.Ok(42)
}
}
fn process(s: str) Result<i32, str> {
val n = parse_int(s)? // エラーなら即座に return Err(...)
Result.Ok(n * 2)
}
fn main() void {
val result = process("123")
match result {
Result.Ok(v) => io.println("Processed: " + str(v)),
Result.Err(e) => io.println("Error: " + e)
}
}
? の動作
equivalent.ngs
// このコード
fn process(s: str) Result<i32, str> {
val n = parse_int(s)?
Result.Ok(n * 2)
}
// 以下と同じです
fn process(s: str) Result<i32, str> {
val n = match parse_int(s) {
Result.Ok(v) => v,
Result.Err(e) => return Result.Err(e)
}
Result.Ok(n * 2)
}
Option 型
値が存在しない場合の表現:
option.ngs
enum Option<T> {
Some(T)
None
}
使い方
find_user.ngs
fn find_user(id: i32) Option<User> {
if id == 1 {
Option.Some(User { name: "Alice", age: 30 })
} else {
Option.None
}
}
fn main() void {
val user = find_user(1)
match user {
Option.Some(u) => io.println("Found: " + u.name),
Option.None => io.println("User not found")
}
}
Option の ? 演算子
get_email.ngs
fn get_user_email(user_id: i32) Option<str> {
val user = find_user(user_id)? // None なら即座に return None
Option.Some(user.email)
}
エラーハンドリングのパターン
パターン 1: match による分岐
read_config.ngs
fn read_config(path: str) Result<Config, str> {
val content = fs.read_to_string(path)?
val config = parse_config(content)?
Result.Ok(config)
}
fn main() void {
match read_config("config.toml") {
Result.Ok(config) => {
io.println("Loaded: " + config.name)
},
Result.Err(e) => {
io.println("Failed to load config: " + e)
std.process.exit(1)
}
}
}
パターン 2: エラー値の変換
app_error.ngs
enum AppError {
IoError(str)
ParseError(str)
NotFound(str)
}
fn read_config(path: str) Result<Config, AppError> {
val content = fs.read_to_string(path)
.map_err(fn(e: str) AppError { AppError.IoError(e) })?
val config = parse_config(content)
.map_err(fn(e: str) AppError { AppError.ParseError(e) })?
Result.Ok(config)
}
パターン 3: エラーチェーン
process_file.ngs
fn process_file(path: str) Result<str, str> {
val content = fs.read_to_string(path)?
val lines = content.split("\n")
val result = lines
.filter(fn(line: str) bool { line.len() > 0 })
.map(fn(line: str) str { line.to_upper() })
.join("\n")
Result.Ok(result)
}
panic
unrecoverable なエラーには panic を使用します:
divide_or_panic.ngs
fn divide_or_panic(a: f64, b: f64) f64 {
if b == 0.0 {
panic("Division by zero")
}
a / b
}
エラーハンドリングのベストプラクティス
1. Result を使い忘れない
best_practice.ngs
// 悪い例
fn main() void {
fs.write_to_string("output.txt", "data") // エラーが無視される
}
// 良い例
fn main() void {
match fs.write_to_string("output.txt", "data") {
Result.Ok(_) => io.println("File written"),
Result.Err(e) => io.println("Write failed: " + e)
}
}
2. エラーメッセージにコンテキストを含める
context.ngs
// 悪い例
fn read_user(id: i32) Result<User, str> {
val data = fs.read_to_string("users.json")?
// エラー時に "No such file or directory" しか分からない
}
// 良い例
fn read_user(id: i32) Result<User, str> {
val data = fs.read_to_string("users.json")
.map_err(fn(e: str) str {
"Failed to read users.json: " + e
})?
// エラー時に何が起きたか分かる
}
3. unwrap の使用を避ける
unwrap.ngs
// 悪い例(ランタイムで panic する可能性)
val config = read_config("config.toml").unwrap()
// 良い例
match read_config("config.toml") {
Result.Ok(config) => use_config(config),
Result.Err(e) => {
io.println("Error: " + e)
std.process.exit(1)
}
}
エラーハンドリングフロー
関数呼び出し
↓
Result<T, E> を返す
↓
match で分岐
├─ Ok(v) → 正常処理を続行
└─ Err(e) → エラー処理
↓
? 演算子で伝播
↓
上位関数で処理