Dev Study
← どの言語にもある仕組み — 値・型・数・文字・エラー・メモリ・並行
レッスン 9 / 12·約 9 分·第25期 どの言語にもある仕組み

エラーの無視は、どれだけ目立つか

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

このコースは、同じ書き方でも何が起きるかはその言語が約束した分しか決まっていない、という1本の線でつながっています。今回は失敗の知らせ方を、無視したときの見え方で比べます。

まず結論

エラーを戻り値で返す言語でも、例外で投げる言語でも、エラーは無視できます。違うのは、無視するために何を書く必要があり、無視したあとに何が残るかです。

Go は `_` で捨てても何も言わず、Rust は捨てた Result に警告を出しますがビルドは止めません。Python は `except Exception:` 1行で失敗を正常値に変えられ、Node では async 関数の中の throw が同期の try に届かず、誰も受け取らなかった失敗が既定ではプロセスを終了させます。

? 読む前に予測

Go で `n, _ := strconv.Atoi("12x")` と書き、エラーを `_` で捨てました。`go vet` にかけてから実行すると、何が起きたでしょうか(Go 1.27.1)。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

無視するのに、何を書く必要があるか

失敗の知らせ方は大きく2つです。戻り値で返す(Go・Rust)か、例外として投げる(Python・JavaScript)か。

戻り値方式の弱点は、Rust の標準ライブラリ文書がそのまま書いています。

A common problem with using return values to indicate errors is that it is easy to ignore the return value, thus failing to handle the error. Result is annotated with the #[must_use] attribute, which will cause the compiler to issue a warning when a Result value is ignored.

戻り値は、受け取らなければそれで終わりです。Rust はそこに警告を足しました。ただし同じ文書のとおり、それは warning であり、既定で unused_must_use という lint が出すものです。

Go は別の立場を取ります。Go blog の見出しは Errors are values. で、続けて Values can be programmed, and since errors are values, errors can be programmed. と書きます。エラーは特別扱いされない普通の値なので、普通の値と同じく _ で捨てられます。仕様は _ を it does not introduce a binding and thus is not declared と定めています。

例外方式では逆に、何も書かなければ失敗は上へ伝わり、最後はプロセスが止まります。無視するには、捕まえるコードを書く必要があります。

無視するために書くものと、無視したあとに残るもの(Python 3.14.7 / Node v24.21.0 / Go 1.27.1 / rustc 1.98.1、Docker aarch64)
Go:エラーを受け取る変数を _ にする何も書かなくても警告は出ない
Rust:Result を受け取らない警告が出る。let _ = で黙る
無視したあと
0 のまま進み exit 0go vet も exit 0
0 のまま進み exit 0警告はビルド時だけ
例外方式では、無視するのに書くもの
Python:except Exception で 0 を返す壊れた入力も 0 になる
Node:async 関数を同期の try で呼ぶcatch に入らない
  • ・戻り値方式は「書かないこと」で無視できる。Rust だけが、書かないことに警告を付けている
  • ・例外方式は「書くこと」で無視できる。書かずに済む道(非同期)もある
具体実物

Go と Rust で、捨てた値に何が起きたか

実演は Docker の aarch64 コンテナで、Go 1.27.1 と rustc 1.98.1 を使いました。どちらも2回実行し、出力は同一でした。

package main
import ("fmt"; "strconv"; "os")
func main() {
	n, _ := strconv.Atoi("12x")
	fmt.Println("n =", n)
	os.Remove("/no/such/file")      // 戻り値を捨てても何も言われない
	fmt.Println("done")
}
go vet のあと go run(exit= の行は実行スクリプトが付けたもの)コマンド出力
vet exit=0
n = 0
done
exit=0

12x は数ではないので Atoi は失敗し、エラーと 0 を返しています。その失敗は _ に消え、n = 0 が正常値として流れました。存在しないファイルの削除も失敗しているはずですが、戻り値ごと捨てた行は何も出していません。

同じことを Rust で書きます。

fn main() {
    "12x".parse::<i32>();
    std::fs::remove_file("/no/such/file");
    let _ = std::fs::remove_file("/no/such/file");
    let n: i32 = "12x".parse().unwrap_or(0);
    println!("n = {}", n);
}
rustc main.rs のあと ./main(出力は無加工。exit= の行は実行スクリプトが付けたもの)コマンド出力
warning: unused `Result` that must be used
--> /w/main.rs:2:5
|
2 | "12x".parse::<i32>();
| ^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
|
2 | let _ = "12x".parse::<i32>();
| +++++++
 
warning: unused `Result` that must be used
--> /w/main.rs:3:5
|
3 | std::fs::remove_file("/no/such/file");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
help: use `let _ = ...` to ignore the resulting value
|
3 | let _ = std::fs::remove_file("/no/such/file");
| +++++++
 
warning: 2 warnings emitted
 
rustc exit=0
n = 0
exit=0

警告は2行目と3行目の2件で、let _ = と書いた4行目には出ていません。警告を消す方法として rustc 自身が let _ = ... を提案しています。そして rustc は exit 0 で、生成されたプログラムは n = 0 を出して終わりました。

Rust で変わったのは、何も書かずに捨てると警告が出る点です。黙って捨てるなら let _ = か unwrap_or(0) のように、捨てたことが読める形を書く必要があります。Rust が足しているのは、警告と、捨てたことが読める目印までです。

なお unwrap と expect は捨てる書き方ではなく、Err のときに panic させる書き方です。Rust Book はこれを試作段階で使い、後でエラー処理を書くときの目印になるものとして紹介しています(They leave clear markers in your code for when you’re ready to make your program more robust.)。

✓ ここまでの確認

Rust で `"12x".parse::<i32>();` の Result を捨てると、rustc は警告を2件出しました。このプログラムはどうなりましたか。

抽象原理

例外でも、無視は1行で書ける

では例外方式なら無視は目立つのでしょうか。捕まえなければ止まるのは確かです。しかし捕まえる側は、何を捕まえるかを選べます。

Python のチュートリアルはこう注意しています。

Exception can be used as a wildcard that catches (almost) everything. However, it is good practice to be as specific as possible with the types of exceptions that we intend to handle, and to allow any unexpected exceptions to propagate on.

except Exception: のあとに return 0 を置けば、どんな失敗も正常値に変わります。Go の _ と同じく、無視は1行です。チュートリアルが勧める形は逆で、The most common pattern for handling Exception is to print or log the exception and then re-raise it ——記録してから投げ直す、です。

もう1つの抜け道は非同期です。async 関数の中の throw は、呼び出しの外へは投げられず、返す Promise を reject 状態にするだけです。ECMA-262 の AsyncBlockStart は、本体が throw で終わると Perform ! Call(promiseCapability.[[Reject]], …) を行い、呼び出し元を NormalCompletion で再開すると定めています。f() は例外を投げずに Promise を返すので、同期の try には何も届きません。誰も受け取らなかった reject を実行環境が見つけるのは、その後です。この場合は無視を書いてすらいません。書き忘れが、そのまま無視になります。

つまり目立ち方は、方式ではなく2つで決まります。無視するのに何を書く必要があるかと、書いた跡を後から探せるかです。

具体実物

Python と Node で、失敗がどこへ行ったか

Python 3.14.7 です。

import json
def load(s):
    try:
        return json.loads(s)["n"]
    except Exception:
        return 0
print(load('{"n": 5}'), load('{"m": 5}'), load('not json'))
int("12x")
print("unreached")
python a.py(exit= の行は実行スクリプトが付けたもの)コマンド出力
5 0 0
Traceback (most recent call last):
File "/w/a.py", line 8, in <module>
int("12x")
~~~^^^^^^^
ValueError: invalid literal for int() with base 10: '12x'
exit=1

{"m": 5} は JSON としては正しく、キーが違うだけです。not json は読めもしません。別々の2つの失敗が、同じ 0 になりました。 捕まえなかった int("12x") のほうは traceback を出し、unreached を出さずに exit 1 で止まっています。

Node v24.21.0 です。

async function f() { throw new Error("boom"); }
try { f(); } catch (e) { console.log("caught?", e.message); }
console.log("after try");
node a.mjs(既定の設定。exit= の行は実行スクリプトが付けたもの)コマンド出力
after try
file:///w/a.mjs:1
async function f() { throw new Error("boom"); }
^
 
Error: boom
at f (file:///w/a.mjs:1:28)
at file:///w/a.mjs:2:7
at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
at async node:internal/modules/esm/loader:647:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
 
Node.js v24.21.0
exit=1

caught? は一度も出ていません。after try が先に出て、そのあと Error: boom で exit 1 です。try の中で f() は正常に Promise を返しており、誰も受け取らなかった reject を Node が検出したのは after try の後だったことが、出力の順番に表れています。

落ちたのは言語仕様ではなく Node の設定によるものです。v24 の CLI 文書は --unhandled-rejections の既定 throw を If this hook is not set, raise the unhandled rejection as an uncaught exception. This is the default. と書き(unhandledRejection のハンドラが無ければ uncaught exception にする)、履歴に v15.0.0 Changed default mode to throw. Previously, a warning was emitted. とあります。同じコードを --unhandled-rejections=warn で動かすと、警告を2件出して exit 0 で続きました。その警告文は原因を throwing inside of an async function without a catch block と名指ししています。

✓ ここまでの確認

Node v24.21.0 で `async function f() { throw new Error("boom"); }` を `try { f(); } catch (e) { ... }` で呼び、その後に `console.log("after try")` を置きました。既定の設定で何が起きましたか。

⇄ 2つを見比べる
具体例 A

Go の n, _ := strconv.Atoi("12x")

Atoi の失敗は _ に消え、n は 0 のまま流れました。go vet は exit 0、実行も exit 0。跡は _ の1文字で、呼び手には正しい 0 と区別できない 0 が届きます。

具体例 B

Python の except Exception: return 0

キー違いと JSON でない入力が、どちらも 0 になりました。例外は外へ出ず、exit もしません。跡は except Exception の1行で、呼び手には 5 と同じ種類の値として 0 が届きます。

一方は戻り値方式、他方は例外方式です。それでも同じ結果になったのはなぜでしょうか。

失敗が起きた地点から呼び手に届くまでの間で、何が失敗を正常値に変えたかを探してください。

この章のまとめ

  • 戻り値方式の弱点は「受け取らなければ無視になる」こと。Rust の std 文書がそう書き、Result に #[must_use] を付けている。
  • Go の _ で捨てたエラーは、go vet でも実行でも何も出なかった(Go 1.27.1)。n = 0 が正常値として流れた。
  • Rust は捨てた Result に警告を出すが、rustc は exit 0 でビルドした。let _ = と書けば警告も消える。約束しているのは「目印が残る」まで。
  • Python の except Exception: return 0 は、別々の失敗を同じ 0 にした。例外方式でも無視は1行で書ける。
  • Node v24 で async 関数を同期の try で呼ぶと catch に入らず、after try のあとに exit 1。落ちるのは v15 からの既定で、warn にすれば続く。
  • 目立ち方を決めるのは方式ではなく、無視に何を書く必要があるかと、その跡を後から探せるか。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

「エラーを戻り値で返す言語なら、握りつぶしは起きない」という主張について、実演から言えることはどれですか。

用語集(5語)
戻り値によるエラー
失敗を関数の戻り値の一部として返す方式。Go の error、Rust の Result。呼び手が値を見なければ、それだけで無視になる。
例外
失敗を呼び出しの外へ投げ、捕まえる場所まで伝える方式。Python・JavaScript。捕まえなければ上へ伝わり、メインの処理では最後にプロセスが止まる。
#[must_use]
Rust の属性。付いた型の値を使わずに捨てると、コンパイラが警告を出す。Result に付いている。警告であってビルドは止めない。
空白識別子 (_)
Go で値を受け取らずに捨てるための識別子。仕様上は束縛を作らない。エラー専用ではない。
処理されない reject
Promise の失敗を誰も受け取らないまま残った状態。Node の既定(v15 以降)では、unhandledRejection のハンドラが無ければ uncaught exception として扱う。