なぜJavaScriptのDateコンストラクタは例外を投げないのか

**Q.** なぜJavaScriptのDateコンストラクタは例外を投げないのか？
**A.** `NaN`があるから

---

`Date`はJavaScriptで日時を扱うためのAPIで、JavaScriptの当初から存在します。

`Date`オブジェクトは主に`Date`コンストラクタを用いて作られます。`Date`コンストラクタにはいろいろな機能があり、`new Date()`のように引数なしで呼び出すと現在時刻を取得できるほか、`new Date("2020-04-24T00:00+09:00")`のように文字列から日時に変換したり、`new Date(1587654000000)`のように数値（UNIX時間）を日時に変換したりすることができます。

一般に、データの変換作業には失敗が付き物です。しかし、`new Date`は決して失敗しません[^note_1]。例えば、`new Date("foobar")`のように明らかに日時を表していない文字列から`Date`オブジェクトを作ろうとしても、ちゃんと`Date`オブジェクトが得られます。これは一体どうしてなのでしょうか。実は、その裏には**`NaN`**が関わっています。

[^note_1]: 正確には、与えられた引数がオブジェクトだった場合はプリミティブに変換する工程が入るので、そのタイミングで失敗することがあります。しかし、それは`Date`とは話が違いますよね。

`Date`は時刻を表すオブジェクトですが、その内部表現は数値（UNIX時間）です。これは、1970年1月1日を`0`として、そこから経過したミリ秒数によって日時を表す表現方法です。そして、数値ということは`NaN`の可能性があるということなのです。例えば`new Date("foobar")`のようなものは、内部の数値が`NaN`である`Date`オブジェクトが結果として得られます。このような`Date`オブジェクトは、数値に変換すれば`NaN`となり、文字列に変換すれば`"Invalid Date"`となります。

つまるところ、どんな値を与えられても`new Date`が失敗しないのは、不正な値が与えられたら`Invalid Date`を表す`Date`オブジェクトにすれば良いからなのです。これは、`Number`で数値に変換する処理が失敗しないのと本質的に同じです。`Number`の場合も、数値に変換できないものが与えられたら`NaN`にすることで、常に数値への変換を成功したように見せかける（実際には`NaN`という結果が返ってくるが）ことができます。

## 最近のAPIは例外を投げる

データ変換時に例外が発生しないのが`Number`や`Date`の特徴でしたが、最近のAPIはきちんと例外を投げる傾向にあります。例えば`BigInt`には`NaN`のような概念はなく、`BigInt`は必ず何らかの整数を表しますから、整数に変換できない値から`BigInt`を作ろうとすると、`BigInt`の値を生成するのが不可能であるためこのように例外が発生します。

<img width="676" alt="スクリーンショット: BigInt(1.5)がエラーになる様子" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/dbda6750-9701-c063-2930-576ea54832c3.png">

また、新しい日時処理APIとして期待されている[Temporal](https://github.com/tc39/proposal-temporal)においても、不正な日時の場合はコンストラクタで例外が発生する仕様となっています。これらのことから、データが不正なときはちゃんと例外を投げるのが今時のトレンドと言えるでしょう。

他の例としては、ES2015で追加されたシンボルを文字列に変換しようとするとエラーになるという例が挙げられます。

```js
const s = Symbol();
console.log(`Hello, ${s}!`);
```

これは`s`を文字列に変換しようとした際に`Cannot convert a Symbol value to a string`というエラーが発生します（Google Chromeの場合）。文字列の変換がエラーになるというのはこれまでには無かった挙動です[^note_2]。この挙動の背景には、シンボルはプロパティ名として使用できる文字列とは別個の概念であるため、文字列に変換できるのは混乱の元であり避けるべきであるという考えが見てとれます。ただし、`String(s)`のように`String`を用いて文字列に変換する場合はシンボルに対する専用の処理が入るためエラーを出さずに文字列に変換できます。

[^note_2]: `toString`メソッドにエラーを仕込むようなことをすればもちろん可能ではありましたが、ここではそういう話をしているのではありませんね。

JavaScript本体以外でも、例えば[WHATWG URL](https://url.spec.whatwg.org/#url-class)も`new URL(url)`としたときに`url`をURLとしてパースできなければ例外が発生します。

## 大域脱出と型の意味

例外の発生（`throw`文や処理系による例外）は**大域脱出**を伴います。大域脱出とは、その場でプログラムの実行を中断して外側のスコープへと制御を移すことを指します。JavaScriptの場合、例外の発生により大域脱出した実行コンテキストは、`try`文に到達するまたは`async`関数の境界に到達するまで外へ脱出し続けます。

ポイントは、例えば関数から脱出した場合、その関数の返り値を読む者はいないということです。

```ts
// BigIntがエラーを発生させるため、変数valに何かが代入されることはない
const val = BigInt("foobar");
```

逆に言えば、`BigInt`の呼び出しが成功した（例外を発生させなかった）ならば、その返り値は必ず“正しい”`BigInt`値であるということです。

そもそも`BigInt`の呼び出しを成功させなければ`BigInt`値が手に入らないのですから、`BigInt`値として存在しているならばそれは必ず正しい`BigInt`であるということになります。これが、`NaN`という微妙な値を擁する`Number`との決定的な違いです。

すなわち、`Number`と`BigInt`ではその型に込められた意味が異なります。`Number`型というのは「`NaN`という不正な値かもしれない値」である一方、`BigInt`型というのは「必ず何らかの整数である値」なのです。`Number`は「失敗しない」という利点を得た代わりに値自体に失敗が埋め込まれている一方、`BigInt`は失敗する代わりに値自体は失敗という概念を持ちません。数値を表すデータという観点からは、後者の方がより優れた設計であることは言うまでもありません[^note_3]。

[^note_3]: `NaN`というのはIEEE 754に由来する概念ですから仕方ない面もあるのですが、ここでは純粋に設計がどうかという観点で議論しています。

他の例でも、例えば`new URL(url)`は例外を投げる可能性がありますが、これによって`new URL`が成功した場合に得られる`URL`オブジェクトは「正しいURLを表すオブジェクト」であることが担保されています。これにより、我々は「`URL`オブジェクトが表すURLが不正なURLである」といった可能性を除外することができます。

同様の考え方は、データ型の設計だけでなく「失敗する可能性がある関数」にも適用できます。JavaScriptでは失敗を表す方法は大きく2つあります。一つはnullやundefinedを返すとか、Option的なオブジェクトを自分で用意するとかにより「失敗を表す値」を返すという方法です。もう一つは例外を投げることです。非同期処理（結果が`Promise`の場合）は前者に相当しますが、async/await構文により後者のような扱い方もできます。

「無いかもしれないデータを取得する関数」を題材にTypeScriptで考えてみましょう。前者の設計の場合、この関数は例えば`string | undefined`型の返り値を持つでしょう。

```ts
function getDataOrUndefined(): string | undefined {
  if (hasData()) {
    return getData();
  } else {
    return undefined;
  }
}
```

一方、後者の設計の場合は返り値が`string`型となります。

```ts
function getDataOrThrow(): string {
  if (hasData()) {
    return getData();
  } else {
    throw new Error("無いよ");
  }
}
```

後者の設計では、失敗に関する情報が返り値から消えました。なぜなら、失敗の場合は`getDataOrThrow`は返り値を返すことが無いからです。このように、大域脱出はインターフェースをシンプルにする効果を持ちます。もちろん、その代わりに大域脱出してしまうことによる辛さはありますが。実際、[ReactのConcurrent Mode](https://qiita.com/uhyo/items/4a6315bfccf387407631)は、大域脱出を活用してインターフェースをシンプルにするという方向に舵を切っています。

## データの意味

最初の`Date`や`Number`に話を戻すと、これらは失敗しても例外を投げませんが、それにも関わらず`new Date()`の返り値は`Date`であり`Date | undefined`などではありません。これはすでに説明した通り、わざわざ失敗の可能性を表す値を用意しなくても`Date`型の中に失敗を表す値（`Invalid Date`）が埋め込まれているからなのです。これらは特殊な例であり、この設計により`Date`や`Number`のデータとしての信頼性が低下してしまっています。

なお、この記事では`Date`などと対比して例外による失敗の表現に言及しましたが、実は`Date`や`Number`などはJavaScriptに`try-catch`文が導入される（ECMAScript 3）より前から存在しました。これがない時代においてはランタイムエラーは復帰不可能なものですから、コンストラクタでランタイムエラーを発生させた場合はその失敗を検知する手段がありません。

歴史的経緯という観点からは、これが`Date`コンストラクタが例外を発生させない直接的な理由と言えるかもしれません。ただ、ランタイムエラーという概念自体は当然最初から存在していたので、別に妥当な日時表現かどうかチェックする関数を用意しておいて、変換できないデータが`new Date`に与えられたらランタイムエラーというデザインも可能だったでしょう。これは当初のJavaScript自体の方向性にも関係していますが、`Date`というデータ型に失敗という状態の表現を含むという意思決定はどこかで行われたのだと考えられます。

## まとめ

データ変換やコンストラクタの失敗時に例外を投げるのは、失敗を表す値を返さないという設計上の選択によるものです。これにより、`BigInt`呼び出しの結果得られた`BigInt`値や`new URL`呼び出しの結果得られた`URL`オブジェクトは常に“正しい”データであることが保証されます。コンストラクタが例外を投げるか投げないかということは、そのデータ型の意味にまで影響を与えるのです。

`Date`はやや特殊な例であり、`new Date`の結果は常に`Date`オブジェクトであり、かつ本来失敗するはずの処理なのに例外を投げないという選択をした結果、`Date`オブジェクトに失敗の可能性が含まれるようになってしまいました。