Skip to content

Instantly share code, notes, and snippets.

@ndxbn
Last active August 12, 2020 02:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ndxbn/163e8da50f64d6819f96ea3457a48a68 to your computer and use it in GitHub Desktop.
Save ndxbn/163e8da50f64d6819f96ea3457a48a68 to your computer and use it in GitHub Desktop.
Error や Exception について考えた。 https://qiita.com/ndxbn/items/b7c9dfa51aeccdbe49b2
title tags author slide
PHP の Error/Exception の分類方法やまとめ方を考えてみたときのメモ。
PHP JavaScript error exception メモ
ndxbn
false

LogicException と RuntimeException の、子クラスの関連

Logic Runtime 意味
Domain Range 変数が、事前に定義された範囲から逸脱してる。
Out Of Range Out Of Bounds 無効なインデックス、キー。
Invalid Argument Unexpected Value 関数側か、関数の呼び出し側か。
Length Overflow / Underflow すでに長さが変だった / 操作したら長さが変になる。
説明 Logic Runtime 説明 共通してそうなもの
a value does not adhere to a defined valid data domain. Domain Range indicate range errors during program execution. Normally this means there was an arithmetic error other than under/overflow. 変数が、事前に定義された範囲から逸脱してる。
when an illegal index was requested. Out Of Range Out Of Bounds if a value is not a valid key. 無効なインデックス、キー。
xxx Invalid Argument Unexpected Value xxx 関数側か、関数の呼び出し側か。
if a length is invalid. Length Overflow / Underflow xxx すでに長さが変だった / 操作したら長さが変になる。

LogicException と RuntimeException

https://www.php.net/manual/ja/class.logicexception.phphttps://www.php.net/manual/ja/class.runtimeexception.php

「何を修正すべきか」で考える

LogicException は 「プログラムのロジック内でのエラーを表す例外です。 この類の例外が出た場合は、自分が書いたコードを修正すべきです」。

RuntimeException は「実行時にだけ発生するようなエラーの際にスローされます」。 これは、LogicException と比べて考えてみると、「運用で作られたデータや、ネットワークが切れたなど、外部要因によって発生する」。 入力されたデータ、渡された引数、実行環境、前提となる条件や要件 などを修正すべき。

「起因するもの」で考える

「データ構造」と「アルゴリズム」で考えがちかなって思ったのだけれど、 Logic というのは「データ構造とアルゴリズム のセットで成り立つもの」なので、そうではない。

LogicException は内部要因で発生する例外。 RuntimeException は外部要因で発生する例外。 ここでいう外部と内部は、コード実装者にとっての外部内部。

例えば「ゼロ除算」はその変数の型を「ゼロ以外の整数」と定義すれば LogicException になる。なので、前提条件というか、時と場合によって外部・内部は変わるんじゃないかなぁと重たt。 しかし、インターフェース設計をするときに、わざわざ「ゼロ以外の整数」はあまりにも Promitive な型なので、ユーザ定義型を用意するのはコスパ悪く、普通はやらない。

例外ケースになることのほうが圧倒的に多いことが容易に想像できるのなら、型を定義するでしょうけれど、その型定義の中で使う例外はなにになるんだい?という話になる。

観点が悪い。

Domain と Range

単純に、LogicException か RuntimeException か。

数学用語の Domain(定義域: x の値の範囲)と Range(値域: y の値の範囲)と考えると、それぞれが「関数の入力・出力」に対応していることがわかる。 Domain は関数に渡す前に「あれ、なんかへんだな」とわかるし、Range は「実行して戻ってきた値が思ってたのと違う」ということ。

Domain は「事前に定義されたドメインデータ」に含まれない、という例外。 Range は「事前に定義された境界が明確なデータの範囲」から逸脱する、という例外。

RangeException の説明に「アンダーフローやオーバーフロー以外の計算エラーが発生した」とあるので、「なにか操作をしようと思ったら変だった(Domain)」「なにか操作をしたら、変になった(Range)」と言い換えることもできる。 (「ドメインデータに含まれない」は、「使おうと思ったらデータがおかしかった」と言い換えることができるので。)

Domain と Range が同じ意味合いで対応する という話は、RangeException の説明に「これ(RangeEaception)は実行時版の DomainException です」とあるので確定。

OutOfRange と OutOfBounds

単純に、Logic Exception か Runtime Exception か。

Valid な値が、列挙するのが馬鹿らしいほどの数がある場合に使う例外。 データを列挙できるかできないか?という点で、Domain/Range と OutOfRange/OutOfBounds を使い分ける。(という使い分けでいいのか?)

OutOfRange は、配列やタプルに対して、インデックス(ほとんどの場合で自然数)に入っているデータが決まっている場合に、未定義な場所を参照しようとして発生する Null Pointer Exception 的なものだと思う。 例えば foo: [string, number, string] というタプルにおいて foo[5] とアクセスした場合に OutOfRange になる。 PHPではタプルを型定義できないけれど、タプルの不整合はコンパイル時に検出可能なので、ドキュメントに書かれている通り「(本来なら)コンパイル時に検出できる」ということになる。結局の所、実装者のミスなので、 LogicException に属していることに納得感がある。

TypeScript の場合、例で挙げたような foo: [string, number, string] というタプルにおいて foo[5] とアクセスしようとすると、コンパイル時にエラーになるので、コードで OutOfRangeExceptionthrow する機会がない。

OutOfBounds は、配列やタプルではなく、連想配列や Map に対して、使おうとしたキー名が Invalid な場合に発生する。 これは型で表現できない動的な情報なので、 RuntimeException に属することに納得感がある。

InvalidArgument と UnexpectedValue

Invalid Argument は呼び出された関数側で throw する。 普通に引数の Validation したときに Invalid なら投げる。

Unexpected Value は関数を呼び出した側で throw する。 関数を呼び出して戻ってきた値が意図した通りになっているかを確認する用途。

例えば https://github.com/sebastianbergmann/diff/blob/master/tests/Utils/UnifiedDiffAssertTrait.php みたいなやつ。

フォームから送られてきた「セレクトボックスやラジオボタンで選んだ値」の改ざんのときは、 DomainException でもいいんじゃね?

そもそも Validation の文脈で例外を投げようっていう発想がダメ

そもそも Domain か OutOfBound で迷ってる時点でおかしいんじゃね?

フロントエンドが実装をミスってる可能性もあるので、「一応確認してくれ」という意図で、Domain Exception にしておくのでもいい気がしている。

サーバサイドから見たら「外部から来たデータ」であることに変わりはないので、LogicException は不適切なので、Out Of Bounds にしよう派なので、自分で実装するときは Out Of Bounds にする。

JavaScript の Error 3種類と、PHP の Exception の対応について

JavaScript には、PHP の例外のような使い方をする(と思われる) Error が 3つ 2つある。

  • TypeError: 値が期待される型でない場合のエラー
  • RangeError: 値が配列内に存在しない、または値が許容範囲にない場合のエラー
  • ReferenceError: 存在しない変数が参照された場合のエラー

これらは、もしかして PHP の Domain/Range OutOfXxx InvalidXxx のそれぞれに対応するんじゃね?と思ったりした。

ReferenceError は Undefined な 変数 にアクセスしようとした場合の話であって、 オブジェクトの Undefined なキー ではない(はず)なので、PHP で言うところの Error レベルの話で、Exception と対比させるのは間違いかもしれない。

PHP Exception JavaScript Error 意味
Domain Range Reference おそらく列挙できるので。ほんとは変数の話だから、 RangeError と見るべきかもしれない。
OutOfRange OutOfBound Range まぁ、普通に範囲エラーだよな。
InvalidArgument UnexpectedValue Type 用途的に考えて同じ。

処理フロー順で発生しそうな順番を考えてみる

  1. 受け取った変数の型がおかしい: InvalidArgument :Logic
    1. 変数の Length がおかしい: Length :Logic
    2. 必要なインデックスがない: OutOfRange :Logic
    3. その他、なにかが明らかにおかしい: Domain :Logic
  2. (なにか処理)
    1. 必要なキーがなくなった: OutOfBounds :Runtime
    2. Length や数値がおかしくなった、なりそう: Overflow Underflow :Runtime
    3. その他、なにかが明らかにおかしくなった: Range :Runtime
  3. 関数呼び出し側でのみ知りうる情報と照らし合わせて、戻ってきた値がなにかおかしい: UnexpectedValue

こうやってみると、 Length や Domain になっているものがなぜ InvalidArgument ではないのか?という疑問が出そう。 Length や Domain は InvalidArgument のより具体的な例外 って扱いなのかな?

Domain/Range は、コンパイル時に検出可能・不可能って話をしているのだから、意味的には同じになるけれど、実装レベルでは全然違うタイミングになる、みたいなアレはあるかも?わからん

継承関係を再整理すると、こうなるんじゃね?

付録

PHP の Error/Exception の継承関係のクラス図

生成元の Plant UML のソースはこちら

@startuml
left to right direction
interface Throwable
class Exception implements Throwable
class Error implements Throwable
class ErrorException extends Exception
class ArgumentCountError extends TypeError
class ArithmeticError extends Error
class AssertionError extends Error
class DivisionByZeroError extends ArithmeticError
class CompileError extends Error
class ParseError extends Error
class TypeError extends Error
class LogicException extends Exception
class BadFunctionCallException extends LogicException
class BadMethodCallException extends BadFunctionCallException
class DomainException extends LogicException
class InvalidArgumentException extends LogicException
class LengthException extends LogicException
class OutOfRangeException extends LogicException
class RuntimeException extends Exception
class OverflowException extends RuntimeException
class RangeException extends RuntimeException
class UnderflowException extends RuntimeException
class UnexpectedValueException extends RuntimeException
class OutOfBoundsException extends RuntimeException
@enduml
@startuml
left to right direction
interface Throwable
class Exception implements Throwable
class Error implements Throwable
class ErrorException extends Exception
class ArgumentCountError extends TypeError
class ArithmeticError extends Error
class AssertionError extends Error
class DivisionByZeroError extends ArithmeticError
class CompileError extends Error
class ParseError extends Error
class TypeError extends Error
class LogicException extends Exception
class BadFunctionCallException extends LogicException
class BadMethodCallException extends BadFunctionCallException
class DomainException extends InvalidArgumentException
class InvalidArgumentException extends LogicException
class LengthException extends DomainException
class OutOfRangeException extends DomainException
class RuntimeException extends Exception
class OverflowException extends RangeException
class RangeException extends RuntimeException
class UnderflowException extends RangeException
class UnexpectedValueException extends RuntimeException
class OutOfBoundsException extends RangeException
@enduml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment