Techfeed conference 2022 で Node.js 最新動向について話した

speakerdeck.com

表題の通り話してきました。

本当は fetch の深い話とかもしたかったんですが、一応ライトニングトークということでほとんど話せなかった。なので、来月 Node 学園やる予定で、 v18 の話とか最近話題の WinterCG の話とか話したいなーと思ってます。 一旦今は予告的な感じ。詳しくは来月。

Node.js の原罪

Intro

ちょうどタコピーの原罪が流行ってるのでこのタイトルにしたけど結構気に入ってる。

d.potato4d.me

この話を読んでの感想とここまで大きくなった Node.js の振り返りをしようと思う。

どんなプログラミング言語であってもみんなから使ってもらって開発者をハッピーにしたいと思ってる。ただ最初は良かったと思ってた機能がなんか古臭くなったり、他にクールな機能を持ったものが登場したことによって徐々に飽きられていき、最終的に他の言語に乗り換えられる。

まぁどんな言語も同じだと思う。C言語だって生まれた当初はすごくクールでみんなをハッピーにしてた。今丁度「戦うプログラマー」を読んでるが、C++が出てきて、周りのエンジニアが C++ を使おうとするシーンが出てくる。そこで、「あんなの使って何が良いんだ、Cで十分だろ」とWindows NT 開発リーダーのデーブカトラーが言ってたりする。ちょうどその頃(正確には NT リリースの少し後)に JavaScript も生まれている。

タコピーが特定の小学生を幸せにしたいと願ったように、プログラミング言語も開発者を幸せにしたいのだ。

それで言うと、 Node.js がここまで成功したことは喜ばしいことなんだと思う。個人的には 2013-2014年 くらいに Node.js やってますというと、「あんなの流行らないっすよねぇ」とか面と向かって言われたり、「いやーあんなネストが深くなる言語よく使ってるねー」とか言われたりしてたのが嘘のようだ。今はたしかに様々なクラウド環境、色んな場所で Node.js をサポートしてくれるようになっている。一方で何かが流行るという事は別な何かが廃れるという事でもある。 potato4d さんの書いた

いつから僕らは Node.js しか使わなくなったのか。
あれだけ話していた Rails などの多くの Web 技術にときめかなくなったのか。

これは Node.js が流行ったことで元々あった技術を振り返り「これでいいのだろうか」と自問をしているんだろうと思う。基本的にどんな技術であっても何かが流行る時は何かが廃れる時なのだ。

逆に言えば Node.js だっていつか廃れる側に入る。

これはまさに原罪なのではないか。永遠の命を生きることができたはずのアダムとイヴが知恵の実を食べた事で人はいつかは必ず死ぬようになったのと同じく、プロダクトやテクノロジーもいつかは必ず廃れるのだ。

つまり、「呪い」や「原罪」と書いたが、実はなんのことはない、数々のプロダクトやテクノロジーでは日常で起きている流行り・廃りという普通のことなのだ。

何がこの憂いの原因か

一方このポエムの中に流れる憂いのようなものもわかる。

真綿で締め付けられるように少しずつ、でも確実にロックインされていて、いつかそれの終焉が来た時に、自分の手元にあるものが「その技術」しかなかったらどうしようという焦燥感が感じとれる。 ものすごくよく分かる。ちょうど potato4d さんとは一度 1on1 を公開でしていたが、その時にも漠然とした焦燥感を感じた。

www.youtube.com

僕も同じような焦りはある。特にNode.js / フロントエンドの領域はどんどん作成するのが簡単になっていて、ちょっと前までは専門知識が必要になっていたことが、半年後には常識になっていたりする。コモディティ化してきている状況において、人材としての希少価値だったり、専門性みたいなものを追い求めようとすると常に最先端に居なきゃいけない気もするし、一方でそれだとロックインがどんどん進み、いつかこの技術が誰にでも使えるようなものになってしまった時、専門家としての自分の価値がなくなってしまう気もする。

ちなみに Deno に変えるっていう意見も見たが、あまり同意していない。徹底討論したときも思ったが、 Node.js と Deno は非常によく似た機能をどちらも有しており、多少の差はあれど、どっちも同じような所に落ち着きそうに思っている。 Deno は Node.js にとって破壊的なイノベーションではなく、持続的な改善だと認識しており、両者でキャッチアップして変わらない世界観の提供になると思っている*1。なのでまぁ、廃れる時は現代のフロントエンド技術そのものと共に両方廃れると思っている。

yosuke-furukawa.hatenablog.com

じゃあどうするのか

個人開発者としては開き直って使い続けててもいい。ここまで成熟して広まってしまうと延命措置もすごく長くなるし、どこかしらで使い続けられるだろうと思う。すぐに無価値になることはない。 一方で別に新しい技術を追うのも良い。どうあれ一個の技術だけで何でもできるような時代ではない。全然別な新しい技術が新しい革新を産んでいるんだな、と興味を持ったらやってみればいい。

正直どっちでもいいんじゃないかと思っている。

いずれにせよ、 Node.js などの技術 (Deno 含む) が現在の開発者を幸せにしているのであれば、ずっと追ってきた自分としては嬉しい限りだ。ここまで広まったのであれば、今後廃れたり、飽きられて終わってしまうならそれもまた良しと思っている。自分はまだ使えるうちは使っていくし、引き続き新しい機能が出たら紹介するといった動きを継続してやっていきたい。一方で、新しい技術を学ぶこともやめるつもりはない。 Go が流行ってる時に Go をやったり、 Rust が流行ってる時に Rust をやったりしてきた。なにか新しいことを吸収しては吐き出すという事を常にやろうと思っている。

こういう流行りにも乗っかりながら、今の自分の強みとなる技術を磨き続ける事が僕の中での最適解だ。

Node.js 年表

それはそれとして、メンバーとこの potato4d さんのブログ記事について色々話していたら、 Node.js でこういうポエムが書かれるのって昔と比較すると信じられないねっていう話になり、俺たちずっと追ってきたけど、良かったよねっていう話をして懐かしさに浸ってしまった。 今 Node.js が流行っている理由の一つは React や TypeScript による強固な仕組みが整ったことが要因だと思っているが、それが流行るまでの歴史的な年表作りたいなと思ったので、書いてみることにする。

出来事
2009年春 Node.js プロジェクト始動、 Ryan Dahl が v8 にネットワーク I/O を組み込んだアプリケーションを作る。最初の名前は netv8 だった、(微妙に .Net 感もあったからなのか知らないけど) rename されて Node.js になる。
2009年秋 Node.js を jsconf.eu で初公開、一気に注目を浴びる。
2010年 Node.js が Joyent に買われる、 Joyent は Node.js を中心とした PaaS を作っていきたいという思いがあった、 Node.js は安定した収益のもとでコラボレーションしたかった。両者の利害が一致して買収された。
2011年 破壊と創造の時代、 API作っては壊し、破壊的変更が起きまくっていた。色々なライブラリが雨後の筍のように現れ、使われていった。 express / socket.io などがその筆頭。この頃に Node 学園祭も実施される、 Ryan Dahl が日本に来てた。 Guille (Vercel CEO)も来てた。
2012年 いきなりここまで来たけど、破壊と創造の時代を終わらせて、Ryan がリーダー辞めますって言い出した。2代目リーダー (isaacs) に権限を移譲
2012年 Node 学園祭で isaacs が新しい Stream とかの構想を発表、 substack が small module の話と unix philosophy の話をしていた。この頃から小さいモジュールをたくさん作って組み合わせようぜ的なノリになる。
2013年 2代目リーダーを中心に Stream 周りやエラーハンドリング (domains など) 周りで色々と新しいものが追加される。
2013年 この頃くらいから grunt / gulp / yeoman などを筆頭に Node.js がフロントエンドのツールとして使われていく。
2013年 ちなみにこの頃に Rendr というアプリケーションフレームワークが少しだけ脚光を浴びる、今の next.js などがやっている、ブラウザでもNodeでも動く、 Universal な フレームワークがここで登場。この頃から Node 学園祭で取り上げてた。
2013年 古川が 2 代目 日本 Node.js ユーザーグループ代表になる
2014年 年初 isaacs 氏がここに来て npm をちゃんとした会社にする、と言って、リーダーを抜けて 3代目に引き継いだ (TJ Fontaine)
2014年 この頃くらいから browserify などを筆頭に Node.js がフロントエンドの主にモジュール解決周りで使われだす。
2014年 issue / PR がめちゃくちゃ停滞する。API の安定を求められたことといきなり引き継いだリーダーとして導くのが3代目では難しかったように思う。ここに来て、 Joyent という会社の中だけでリーダーを選出していたことが仇になった感じがある。いわゆる GitHub で "Is Node.js dead ?" って聞かれてしまうようなレベルで特に何も改善がないままズルズルと放置されてた
2014年 AWS Lambda が Node.js サポートで初期リリースされる。
2014年 年末 Node.js が fork されて io.js が誕生する。 yosuke-furukawa.hatenablog.com
2015年 年初 BDFL を中心にした Node.js と コミュニティを中心にした io.js で分断が起きる、あっという間に v8 が最新になり、次々と新しい機能が出現した
2015年 io.js と Node.js の再マージ案が出る、このまま分断されるのかどうなるのかを見守っていた自分としてはマージされる運びとなり、一安心、 YAPC で顛末を発表したどうしてこうなった? Node.jsとio.jsの分裂と統合の行方。これからどう進化していくのか? - Speaker Deck
2015年 ES2015 が出て、 babel が支配的になり、トランスパイルに抵抗がなくなる。 React などのフロントエンドライブラリが大きく躍進する。この頃色んなカンファレンスどこも JavaScript の話ししてた気がする。
2015年 electron とかが流行ってた。自分もなんかアプリ作って出したりしていた。
2015年 isomorphic tokyo meetup 開催、 Next.js とかが話されるよりも前に、クライアントとサーバで同じ処理が動くようにすれば Node.js ってもっと面白くなるんじゃね?みたいな話ができてた。
2016年 npm が勝手にライブラリを消したことで leftpad 問題と呼ばれる大きな問題が起こる、この頃から「俺たちなんかたくさんライブラリに依存しすぎじゃね?」みたいな雰囲気になる。
2016年 ES Modules と CommonJS との相互運用についての議論が起きる。 yosuke-furukawa.hatenablog.com 入れる必要ある?いらなくね?いや、いるだろ、ブラウザと同じコード動かすかもしれないんだし、差は少ないほうがいい、みたいな議論がたくさん起きて、一回 Issue がクローズされる。その後、 TC39 や WHATWG を巻き込んだ議論になっていく。
2016年 この頃にはもう browserify じゃなくて webpack とかでやるのが普通になっており、 React / Redux とかアプリケーションを作る方法も変化しながらも確立されていく。僕はこの頃、 React で SSR とか BFF とか言ってた気がする。今でも言ってるか。この年の ISUCON 本戦の問題が確か SSR だった気がする。
2016年 Next.js がリリースされる。微妙にクエリーパラメータでしかルーティングできなかったので、この頃はそこまで流行ってなかったように思う。ただ React の SSR を基本にしたボイラープレートは雨後の筍状態でたくさんあった。
2017年 Node.js に HTTP/2 を入れたり、 async-await が入ってたくさん色んな改善が進む
2017年 React で SSR やるとどうしても Node.js のプロセスが圧迫されるので、パフォーマンス・チューニング系の話が多かった気がする。 async_hooks などの非同期で状況をトレースできるようにする仕組みが入ったり、 Node.js にもパフォーマンス計測系の活動が行われてた気がする。
2017年 ESModules の解決が Node.js でできるようになった。一方でブラウザ側はトランスパイルする事で形だけ ESM を使っているような状況であり、 HTTP/2 が入ったり色々な改善がブラウザに起きていたものの、 ESM が普通に使えるような状況ではなかった。逆に Node.js で先に実装が進んでしまった。
2017年 2016年くらいから TypeScript が VS Code とともに流行る。3rd party モジュールの型を解決する方法に決着が付いたことも大きかったように思う。
2017年 Node.js内でWeb Community との親和性を上げていこうという動きがもっと活発になる。 async-awaitなどのパーツが揃ってきたことで、どんどんフロントエンド開発者にも使われるようになり、なるべく API を揃えようという流れに。
2018年 Deno が jsconf.eu で発表される。 Node.js の10の後悔という内容でめちゃくちゃバズる。 yosuke-furukawa.hatenablog.com TypeScript で開発されており、ものすごく注目を浴びる。
2018年 Node.js の性能やセキュリティ面、デバッグ可能性をあげていこうというものすごく下回りの整備がされていく。 http parser が llhttp になって、なんじゃこりゃーすげーってなったのを覚えてる。
2018年 npm が資金難みたいな感じでなんか色々と開発者が辞めたり、レイオフにあったみたいな話が出てくる。ゴタゴタする。
2019年 npm vs yarn 抗争激化、どっちも zero install とか言い出す。結果は・・・
2019年 npm のレジストリを管理していた人たちが entropic という分散管理レジストリの構想を出す。結果は・・・
2019年 React に hooks が入る。 Redux とかの状態管理周りのやり方が中央集権的なものからコンポーネントの状態へ、分散独立的な流れになった。
2019年 Next.js が動的にパラメータを受け付ける書き方ができるように改善される、他にも Google のメンバーから色々とコントリビューションを受け付けるようになり、一気に大規模化する。自分もこの段階で Next.js を再評価した気がする。
2019年 jsconf.jp 第一回目開催
2019年 Node.js 10周年、ここらあたりで、「あーなんか Node.js って前よりも普通に受け入れられるようになってるんだなぁ」と実感する。
2020年 COVID-19 pandemic になる、ほとんどのカンファレンスが中止、一気に色々とリモートワークに変わる
2020年 npm が github にジョイン、一旦資金難とか開発者不足は解消される
2020年 Deno v1.0 リリースされる。
2020年 Node.js next 10 という次の10周年のためのマイルストーンが出てくる。 fetch / single executable app / ESM 強化 / TypeScript との親和性強化 などの進化が掲げられる
2021年 Node.js に Promise 化や AbortController など Web Standard との親和性に必要なパーツが徐々に揃ってくる。一方で、なんとなく Deno との競争意識からか、本当にいるのか不明なものも入ったりする。 btoa/atob とか。。。
2021年 また少しずつフロントエンド周りの競争が激化する、特に webpack vs esbuild や remix vs next.js 、 deno vs Node.js などの競合と競争関係になる。新陳代謝の時期かもしれない。
2022年 現代へ。この記事書くの大変でした。

こうやって歴史的な年表を書いてみた(だいぶ自分の話もあるが)。けど、いつ流行りが終わるのか、それともまだまだこれからも続くのかという疑問に回答は出なかった。なんとなくだけど、身近な競合がまだまだ雨後の筍のように出てきている現状では廃れるという事はしばらく無さそうではあった。ただ数年後にはわからない。

いずれにせよ、振り返るのも面白かった。

*1:自分はそれが非常に残念である、 Deno が Node.js 互換を提供するとは言ってほしくなかった、ぜんぜん違う世界観を追っていてほしかった。

Node.js / Deno の徹底討論を Node 学園で行いました。

3/17 に徹底討論という形で denoland の 日野沢さん をお呼びして Node学園で徹底討論という形で討論しました。

いくつか面白いトピックがあり、参考になると幸いです。

少しだけ紹介します。

ESM vs CJS

ESM と CJS の対応が Node.js がグダグダだと思っていると言われた点がありました。

討論内でも Twitter を見ていても、そういう意見があって、意外だなーと思いました。

もちろん現実的に ESM / CJS の移行は今はまだ過渡期です。既存のエコシステムを壊さないために CJS との相互運用性を持ち込むしかなかったという状況においては現時点の拡張子やpackage.jsonでの指定で識別可能にし、既存のエコシステムを壊さないように乗り切ったというところはむしろ評価していました。この移行措置が無く、もしも Node.js v20 からは ESM でしかロードできないと言われた場合はどうなっていたでしょう。多分誰も v20 に上げられず、結局 v19 がLTSが終わるまで使い続け、LTSが終わったあともどこか (Red Hat とか) が 3rd party製の Node.js v19 のメンテナンスを名乗り出て、サポートし続けるといった悲劇が生まれていたように思えます。エンタープライズはそれで乗り切ろうとかそういう議論が行われていたでしょう。それがなかったとしても、 このモジュールはESM, このモジュールはCJSといったように自分で見分けるとかそういう事はやってられないと思います。結局 TypeScript でずっとトランスパイルして、 Node.js でも ESM ではなく、しばらく CJS で使われていたのではないでしょうか。

そう思うと非常に今の移行過渡期の状態というのはそこまでグダグダではないと思っています。

でもいま現実的にきついんだけど

それはそのとおりですが、それは Node.js だけのせい、というわけでもないように思えます。結局 ESM は CJS でロードされた JavaScript とは互換性がないものとして設計されてしまっています。むしろ Node.js は被害者で、そこで一定の互換性をプラットフォームとして保ちつつ、新しい仕様に追従していく、というのは非常に根気のいる作業です。個人的には新しく2つめのロード方法を作り直してしまって後はユーザーが選べば良いという位割り切ってしまってもよかったように思えますが、それをせず、どちらから呼ばれても動くようにするために、色々な仕様を TC39 とも調整し、 WHATWG 側にも話を聞きに行ったりしつつ、最初のPRから数年掛けて今の相互運用できる状況にまで達成したことは非常にすごかったな、、、と思っています。

xxx feature is bad.

よく JavaScript を見ていると、この機能が駄目だとか、この機能がイケてないという話をよく目にします。これだから JavaScript は、、、となってしまう気持ちもわかりますが、ちょっと調べてみると奥になんでその仕様になっているかの背景が結構語られています。その内容には歴史的な背景が紐付いていることが多いです。「ウッ」となって文句を書いたり言う前に何故そうなのかを調べてみると面白いと思います。

こういう話をたくさんしました。ぜひぜひ上の動画を見てみて下さい。

2月に発表したいくつかのスライド

2月にいくつか発表したので記録として残しておく。

開発組織の持続可能性について

speakerdeck.com

A Philosophy of Software Design 前半

speakerdeck.com

どちらも最近読んだ本をベースにしている。

読書をベースにした事で情報としての新鮮さは失われているものの、普遍的な話はできている気がしている。

毎月2-3冊を読めていることは非常に効果的だと感じるし、英語の読書を習慣づけていこうと思う。

こういうのも記録として残しておこうと思ってブログにもポストしておく。

Unhandled Rejection の考え方

はじめに

twitter 上で議論になっていたネタを本人の許可を得て記載しています。

実はこの話は会社の中でも一回議論になったネタなんですよね。僕も microtask と呼ばれる Promise キューイングの仕組みとイベントループでタスクをハンドリングする仕組みの両方が組み合わさった時に Unhandled Rejection が起きる理由がわかりにくくなるなーと思っています。誤解していた様子でしたが、それもうなずけます。僕も誤解してました。今回はその理由を補足しながら解説してみます。

Unhandled Rejection

Promise には Pending, Fulfilled, Rejection の3状態があります。Pending は処理中の状態を示し、 Fulfilled と Rejection は Promise が成功したか失敗したかという2つの状態を指しています。 Rejection だった時に catch して処理が行われたのであれば、 Rejection はハンドリングされたものとなります。誰も catch しなかったら Rejection はハンドリングされなかった状態になります。この状態を Unhandled Rejection と呼びます。簡略化のために下記のコード例を記載します。

main()

async function main() {
    await fail(); // Unhandled Rejection
    console.log('Successfully handled')
}

async function fail() {
    throw new Error('failed')
}

4行目で throw されてる例外を誰も catch していないので、 Unhandled Rejection 扱いになります。try-catch.catch などの方法で catch すれば Unhandled Rejection 扱いではなくなります。

じゃあこのときはどうなるのか

// https://gist.github.com/shqld/be21d1e82cec4dccfc933477f9b60356

main()

async function main() {
    const tasks = []

    tasks.push(fail())

    // Commenting out this line makes it work
    await new Promise((resolve) => setTimeout(resolve, 0))

    tasks.push(fail())

    await Promise.allSettled(tasks)

    console.log('Successfully handled')
}

async function fail() {
    throw new Error('failed')
}

該当のスクリプトですね。この時も Unhandled Rejection になります。 main 関数の3行目の await new Promise((resolve) => setTimeout(resolve, 0)) で起きます。一見うまくいきそうに見えるから不思議ですね。しかもこのコードをそのまま Chrome 等で実行してみると特にエラーも何も出ずに成功したかのように見えます。

しかし、 Node.js/deno のときは Unhandled Rejection で process が落ちます。

Error: failed
    at fail (/private/tmp/t3.js:10:11)
    at main (/private/tmp/t3.js:5:11)
    at Object.<anonymous> (/private/tmp/t3.js:2:1)
    at Module._compile (node:internal/modules/cjs/loader:1097:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1149:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47

ただし、 Chrome であったとしても、 ログのレベルを verbose に上げた状態で上のコードを実行すると Unhandled Rejection が発生していることが確認できます。

ログレベルで verbose まで上げておく
ログレベルで verbose まで上げておく

Unhandled Rejection エラーになる (Chrome)
Unhandled Rejection エラーになる (Chrome)

一旦スクリプトを細かく分割して整理しつつ、中でどんな事が起きてるのかを解説します。

もう少し細かく

Unhandled Rejection が起きるといいましたが、一応条件があります。それは Promise が内部的に利用している microtask キューと呼ばれるキューが空になったタイミング で Rejection 状態で handle されてない Promise があった時です。 また、 microtask キューは "queue" と名付けられているだけあって、 FIFO (First In First Out) な動きをします。つまり、queue にタスクが積まれるのですが、それは先頭から消費されていきます。

function main() {
  Promise.resolve().then(
    () => console.log("promise first") // キューに積まれる
  ).then(
    () => console.log("promise second")  // キューに積まれる
  );
  console.log("show first"); // 同期的に実行される
}

main();
// show first
// promise first
// promise second

microtask queue の動き
microtask queue の動き

microtask キューが空になったタイミングで Rejection 状態になると Unhandled Rejection になります。

function main() {
  Promise.resolve().then(
    () => { throw new Error("failed") } // rejection 状態になる
 ).then(
    () => console.log("promise second")  // rejection 状態なので then は通らず、ここは queue に積まれない
  ); // taskqueue が空になった状態で Rejection のままになる。このままだと Unhandled Rejection になる
  console.log("show first"); // 同期的に実行される
}

main();

Unhandled Rejection 状態
Unhandled Rejection 状態

元のスクリプトを見てみる

この状況で元のスクリプトを確認してみましょう。まずは setTimeout の Promise が無い状態だとどうなるかを考えてみます。

main()

async function main() {
    const tasks = []

    tasks.push(fail())

    // Commenting out this line makes it work
    // await new Promise((resolve) => setTimeout(resolve, 0))

    tasks.push(fail())

    await Promise.allSettled(tasks)

    console.log('Successfully handled')
}

async function fail() {
    throw new Error('failed')
}

この状態だと、 Promise.allSettled 関数が throw したエラーを内部的に catch してくれます。 Promise.allSettled はすべての Promise のタスクが Fulfilled にせよ Rejection にせよどの状態になったとしても終了するまでハンドルする関数です。この動きをさせるために内部的には Promise の例外をキャッチしてくれています。なので、この状況だと Promise は Unhandled Rejection にはなりません。普通に終了します。

Promise.allSettled により、Unhandled Rejection にならない
Promise.allSettled により、Unhandled Rejection にならない

ただ逆に最初の tasks.push(fail()) で止めたらどうなるでしょうか。

main()

async function main() {
    const tasks = []

    tasks.push(fail())
}

async function fail() {
    throw new Error('failed')
}

この場合は Unhandled Rejection になります。 microtasks のキューが空になった状態で fail() 内で Error をスローしてそのままだからです。

fail() が Throw してるので Unhandled Rejection
fail() が Throw してるので Unhandled Rejection

さて、 もう一回もとに戻してみましょう。今度は該当の setTimeout を入れてみます。

main()

async function main() {
    const tasks = []

    tasks.push(fail())
    
    await new Promise((resolve) => setTimeout(resolve, 0))

    tasks.push(fail())

    await Promise.allSettled(tasks)

    console.log('Successfully handled')
}

async function fail() {
    throw new Error('failed')
}

さらにもう少し説明を追加します。

Node.jsのイベントループモデルと一緒に解説します。下記のように EventLoop でのタスク管理と Promise でのタスク管理 (図で microtask queue と書いたところ)に分かれています。

EventLoop とタスク管理
EventLoop とタスク管理

緑色の箱で示したフェーズと青い箱で示した nexttick queue / microtask queue が存在しています。各フェーズにもコールバックを貯めておくキューが存在します。それぞれ赤い線で nexttick queue や microtask queue を適宜呼び出し、フェーズが終了した段階で microtask queue に積んだり、 nexttick queue に積んだりします。

上のコード例で言うと、 await new Promise((resolve) => setTimeout(resolve, 0)) の時に setTimeout をしているので、このタイミングで、 EventLoop の Timer のコールバックにタスクが積まれます。この時点で一旦 EventLoop 側に Task が移ります。

setTimeoutしたタイミングで Timer に処理が移るも、そのタイミングで microtask queue が空であると判定され、rejection したままになる
setTimeoutしたタイミングで Timer に処理が移るも、そのタイミングで microtask queue が空であると判定され、rejection したままになる

この際に microtask queue は一旦空になります。空になったタイミングで、 Handle されていない1つ目の fail() 関数が Error を Throw しているため、 Unhandled Rejection 扱いになります。

さぁ、わかりにくいですよね。非常にトリッキーです。Promise の処理が行われた後に 「EventLoop 側の処理機構に移る」という言い方をしていますが、この際に別なフェーズに処理が移ってしまいます。中の青い箱で示したキューから緑のフェーズに処理が移るタイミングで中の Promise のキューがあれば即座に実行されます。その結果、 fail() 内部でエラーが throw されているので Unhandled Rejection 扱いになってしまいます。ちなみに nexttick queue があればそちらを優先します。

setTimeout の内部が呼ばれた時点での microtask の状況
setTimeout の内部が呼ばれた時点での microtask の状況

同じ task の中で同期的に実行していれば (Timer の処理がなければ) Promise の Unhandled Rejection な状況にはなりません。同じタスク内の最後で allSettled などで catch してくれていれば、意図通りに動きます。しかし、その途中で setTimeout, setImmediate などで Timer の処理を動かしたり、 fs でファイルを操作したり、 http リクエストしたりするとその段階で EventLoop の各フェーズに処理が移ります。この時点で一回 microtask queue を実行します。この時に Rejection 状態になってしまっていると、 Unhandled Rejection 扱いになります。

わかりにくくしているポイント

実は Node.js / deno も Chrome もどちらも Unhandled Rejection に一回なっています。ただ Chrome の場合は一回 warn として処理された上で、次の tick に行った場合にそこでちゃんとハンドルされているのであれば、 verbose ログに出すだけで、エラーになりません。

一方で Node.js/deno は一回でも Unhandled Rejection になった場合はエラー扱いになり、落ちます。この動きの差はわかりにくいものの、仕様通りの挙動になっています。

Promise の ECMAScript 上での仕様は Unhandled Rejection 状況になったことを Platform 側に伝えるところまでが明記されています。ただ伝わった後どう Unhandled Rejection を処理するかは Platform 側で自由に決めて良いことになっています。 Chrome のようにクライアントサイドのブラウザであれば、Promise が例外だったとしても warn 扱いで留めた上で、次のイベントループで処理されていればログレベルを下げて表示する、という対策もありでしょう。逆に Node.js/deno はサーバサイドで未処理の例外が発生した状況のままなにも通知せずに動いてしまうとリソースの開放漏れがあったり、例外が発生してレスポンスを返せていないのに正常に動作してるかのように見えるといった、プロセスが落ちるよりもさらにひどい状況になり得ます。そのためプラットフォームごとに動きが異なる、という仕様になっています。非常にややこしいですね。。。

ちなみに Node.js の場合、デフォルトでその動きになっているものの、変更することも可能です。 --unhandled-rejections という起動オプションに対していくつかの設定が用意されています。

  • none UnhandledRejection が起きたことを警告はするが、特に詳細は表示しない。 Chrome とほぼ同じ扱い
  • warn Unhandled Rejection が起きたことを詳細に警告ログに残す
  • throw デフォルトの挙動、エラー扱いとして unhandledRejection イベントを process に通知、何もハンドラがなかったら uncaught exception と同じ扱いとして、エラーにする。
  • strict ハンドラの有無に関わらず、uncaught exception と同じ扱いとして、エラーにする。
  • warn-with-error-code Unhandled Rejection が起きたことを詳細に警告ログに残しつつ、終了時に exit code を 1 にする(異常終了扱いする)。

これらの扱いをうまく使えば Chrome と同じ挙動にすることも可能です。ただし、Node.js/deno といったプラットフォームが提供しているデフォルトの挙動は安全サイドに倒したものになっており、これを意図を考慮しないで変更することは個人的にはオススメしません。プラットフォームのデフォルトの動きがなぜそうなっているのかを把握した上で変更するのであれば、良いと思います。

まとめ

Unhandled Rejection の状況について解説しました。 Timer や fs といった EventLoop の機構と Promise の機構が混在しわかりにくくしている上に、Chrome などのブラウザは Unhandled Rejection なエラーを verbose でしか出していない所が、余計わかりにくくしています。ただ、それにはちゃんとした理由があるという所まで分かっていただけると幸いです。

個人的には Unhandled Rejection 状態になった時の Promise の仕様がプラットフォーム依存になっているところがもう少し周囲が把握できているといいなとは思います。 Chrome と Node/deno だけではなく、 Firefox とかも動きが違うので、本当に難しくなっています。 Node.js に関しては複数の動作パターンを用意していますが、 Unhandled Rejection 時のデフォルトの挙動をどうするべきか議論が白熱したため、このようにオプションできりかえられるようになっています。

今一度 Unhandled Rejection をプラットフォームがどう実装しているのかは確認してみると良いと思います。

参考資料

2021年振り返り

はじめに

今年も書きました。毎年書くことで去年との差分を知れるので、良いですね。来年もちゃんと書きます。

yosuke-furukawa.hatenablog.com

会社

去年は "マネジメントとシニアソフトウェアエンジニア" で4年目になったんですが、今年はそれに経営的なロールまでついて 5 年目突入しました。 大変でしたが、やりがいがある仕事につけて楽しいです。

qiita.com

この手の経営とマネジメントとシニアソフトウェアエンジニア的な話をするタイミングももらえてよかったです。

社内アドベントカレンダーもどちらも活況でした。

adventar.org

qiita.com

優秀なメンバーに恵まれてるなとつくづく感じます。

また、ニジボックス会社内で研修として JavaScript/TypeScript/Next.js 研修をメンバーと一緒にやることができました。

www.wantedly.com

www.wantedly.com

こういう研修が巡り巡って、リクルートの社内研修にも活かされていたりします。

recruit-tech.co.jp

そういえば研修ではブラウザの話も同様にしました。

speakerdeck.com

社内イベント

R-ISUCON 2021 を開催しました。

blog.recruit.co.jp

こういったイベントは完全にオンラインに変わりましたね。オフラインでやってた時のワイワイ感をいかにオンラインでも出すかという所にフォーカスされてきている気がします。 一応このイベントではオンライン飲み会用のケータリングサービスである nonpi を使って食事を配ったりしました。

イベント

JSConf.jp 2021

JSConf.jp 2021 を開催しました。

jsconf.jp

一年越しの開催で、だいぶ難しい形になってしまいましたが、非常に活況だったかなと思っています。 すべてのトークYoutube にスタッフの方が分割してあげてくれています。内容を後から確認できるところも含めて非常に面白いですね。

www.youtube.com

また、発表は StreamYard 、聴講は Youtube Live と Spatial Chat という二段構えで実施していました。もう少し Spatial Chat 側を盛り上げたかったなと思いましたが、また来年やる時にはちゃんと調整します。 一方で非常にオンラインは後片付け等が少なく、スタッフの負担も軽いので、これをどうするかは今後要検討ですね。まだ探り探りですが、来年もオンラインのがいいかなーと個人的には思っています。

東京 Node 学園

3回ほどオンラインで開校しました。

Node学園 35時限目 オンライントライアル - connpass

Node学園 36時限目 オンライン - connpass

Node学園 37時限目 オンライン - connpass

来年もちゃんとやります。オンラインになることでこちらの負担がだいぶ軽くなりましたね。一方でずっと続けてしまうと逆にリアルタイムで見なくていいやと思う人も増えそうなのでそれはそれで避けたい気もしていて、複雑です。

登壇系

Engineer Career Study

スペシャリストになる覚悟を話しました。だいぶ偉そうなことを書いていますが、何度もあの後色んな所で話す機会をいただき、参考になったというフィードバックをもらえてよかったです。

speakerdeck.com

Developer eXperience Day

フロントエンドエンジニアの開発体験について話しました。この時のトークも面白かったですね。他にビッグネームな人たちがたくさんいたことを覚えています。

speakerdeck.com

Node 学園 37 時限目

WHATWG Stream の話をしました。何気に Node.js の発表久々だったかも。

speakerdeck.com

Fastly Yamagoya

性能に関する考え方の話をしました。尊敬するエンジニアが Fastly には多いので、ちょっと張り切りました。

speakerdeck.com

この時に頂いたパーカーがマジですごかった(裏起毛で温かい、この時期外にパーカーだけで出てもなんとかなる)。重宝しています。

Chrome Advisory Board

Chrome 諮問委員会というボードのメンバーなのですが、そこで英語で JSConf.jp の話やどうやってコミュニティを運営しているかを発表させていただきました。

speakerdeck.com

英語で登壇したのですが、割と良いフィードバックをもらえたので、ものすごくありがたい機会でした。割と時間がなかったのですが、英語毎日勉強してたおかげで資料を作る時間はそこまでかからなかったです。

技術顧問勉強会系

この他にも技術顧問で勉強会を開いているのですが、以下の奴が良いフィードバックをもらえました。

レビューの仕方

speakerdeck.com

フロントエンドテストプラクティス

speakerdeck.com

書籍を書く

Web+DB Press vol.123 Next.js 特集

gihyo.jp

Next.js 特集を記述することができました。 @takepepe さんと共著しました

Node.js

CodeGrid

今年も CodeGrid で記事を書きました。

www.codegrid.net

www.codegrid.net

ブログ

急遽アドベントカレンダーを追加して3つくらいエントリーを書きました。問題提起系を1エントリ、あまり知られていない新機能系が2エントリ。

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

Rust

新しい言語を覚えようということで Rust を覚えました。楽しいのでやり続けます。なんか別なことにも使ってみたいですね。

yosuke-furukawa.hatenablog.com

競技プログラミング

Advent Of Code を完走したのと LeetCode を 800 問以上解きました。

LeetCode の戦績
LeetCode の戦績

Advent Of Code 2021
Advent Of Code 2021

yosuke-furukawa.hatenablog.com

英語

DMM英会話マスターレベルになりました。Happy almost New Year!! みたいなことを今日は言い合って終わりました。喋れるようになると色々とネタが出てくるので楽しいです。もっと使いたいですね。

DMM 英会話マスターレベル
DMM 英会話マスターレベル

ELSA Speak もやってました。

プログラミング

なんか競技プログラミングで毎日コードを書いてましたね。

github all green
github all green

とはいえ、もう少しなんか実のある事もやりたいんですよねー。できてないけど、来年はもう少しこの辺りをちゃんとアウトプットします。

まとめ

  • 会社でエンジニアコミュニティづくりをやってます
  • 登壇も頑張った、だいたい毎月一個くらいはなんかやってた気がする
  • 書籍も書いた。 Web DB Press は非常に良かった
  • Node.js 系の記事 5 件位書いた。
  • 新しく Rust を学んだ。
  • 競技プログラミングで LeetCode 800問解いたのと AoC 完走した
  • 英語で DMM 英会話マスターレベルになった。

振り返り

去年目標にしてできたこと: - 新しい言語の習得 - ブログその他のアウトプット

できなかったこと: - 新しい言語でなにか作る

来年の目標

来年の目標: - なにかライブラリやアプリを作る - 人に教える系の何かをちゃんとやりたい - もう少しブログのアウトプットを増やす

最後に

今年も一年間インプットもアウトプットも走り抜けた気がしています。今は少しゆっくりしたいかなーと思っています。 もう少ししたら新年です。個人的には新年になる前の数時間がものすごく好きで、まったり過ごそうと思っています。家族や友人とゆっくり過ごして新年リフレッシュしたいですね。 来年もまたよろしくお願いいたします。

実力なのか運なのか

最近 SNS を見ていて、「人に努力をしろ」と勧めにくくなった、「もっと努力した自分を称えるべきでは」といった内容の投稿を見かけました。 この他にも「運も実力の内」、と言ったり、「運は実力によってカバーできる」といった言説があります。

いくつか本を読んで自分の中で思ったことをまとめて書こうかなと思います。

イメージ

何かしら成功する際に、自分の実力と運が両方必要になるケースがあります。例えばプロのソフトウェアエンジニアになるとして、そもそも実力が足りなかったらなれませんし、一定以上の規模の会社で働こうと思った場合は面接があったりするので、面接官との相性だったり、たまたま出てきたコーディング試験が得意な問題だったといったような事があるでしょう。他にもプロ野球選手になるというケースでも実力はもちろん、たまたまスカウトが見に来た時に運良く活躍するケースもあるでしょう。こういう人のことを「持ってる人」と言ったりしますよね。これは「特別な運を持ってる人」という意味で使われるケースが多い気がしています。

つまり何かしら成功する時に「運」と「実力」の両方が必要だという感覚はほとんどの人が持っていると思います。

運と実力
運と実力

ここで実力というのが足りない時に実力を伸ばし、目標に向かっていくのが「努力」と呼ばれる行動です。 逆に「運」というのはこちらが調整できないモノを指している事が多いです。

努力のイメージ
努力のイメージ

そういう「運」と「実力」を対比した上で、上のイメージで言えば、運の要素に頼らないくらいの実力を身につければ関係ない、という話が「運は実力に寄ってカバーできる」という言説です。

運と実力のイメージは正しいかどうか

さて、実力と運という対比でものを見ていると上のイメージは理解しやすいのですが、実力と努力と運というのはそう簡単なイメージで片付けられるものでしょうか。 最初から実力を強く身につけている人は少なく、何かしら努力して手に入れてきていると思います。ただ努力するという機会に恵まれているという事は、そもそも「こちらが調整できないモノ」である事が多いと思います。家庭環境が裕福だった事によって私立受験する人は多いと思いますが、家庭環境が裕福という事が既に大きいアドバンテージになっています。これは最初から調整できるものではありません。同じようにソフトウェアエンジニアに多い、「家に最初からパソコンがあった人」というのもそういう機会に恵まれた人だと思っています。

つまり、努力と実力と運といった時に結局努力できる機会も「運」によってもたらされているものなのです。

実力部分も運から来ている
実力部分も運から来ている

そういう事を教えてくれた本が、「実力も運のうち、能力主義は正義か」という本です。

www.amazon.co.jp

この本の中では努力を誇ることを「エリートの傲慢」と痛烈に批判します。著者は現代社会の能力偏重な状況を良いと思っていないわけです。本には「そういった能力偏重主義は不平等さを加速させる」という話が出てきます。ただ間違っちゃいけないのは「努力を誇ること」を批判しているのであって「努力そのもの」を批判しているわけではないです。

なんで努力を誇ってはいけないのか

「努力を誇る」というのは成功者の特権のように扱われています。努力したこと自身が大変だったということは理解しますが、実際には努力している影で支えている人たちが居ます。また努力できる機会にすら恵まれなかった人たちも居ます。上述した本の中には「そういったエリートの傲慢さが分断を産み出している」という非難をしています、これが昨今のコロナのワクチンクーデターや brexit 、トランプ政権誕生といった話につなげています。

この本の中で著者は「社会的に評価される仕事の能力を身に着けて発揮すること」は否定していませんし、むしろ推奨しています。一方で、「努力を称えて努力した人としなかった人に優劣をつけること」は否定されています。

人はスタート地点からして不平等であり、その中で実力をつけながら努力することはどうあれ必要です。ただ努力を称えることは能力主義に繋がります。能力主義は「不平等の存在はもはや是正できないため、不平等を正当化する」事で成長を刺激します。ただこれが行き過ぎてしまうと不平等による格差が生まれ、分断が生まれてしまう事を著者は危惧しており、それを社会的に変えていかなければいけないという話に展開しています。

この本自体は割とそういった社会に対してのテーマを語っているため、個人個人でやれることは少ないのですが、一つあるとすると「努力をしたことを誇る」という事をやめて、「周囲の人達へのサポートに感謝し、運が良かっただけである」という自覚をする事ができることなんじゃないかと思いました。

運の利益率

もう一つ本を紹介します。ビジョナリー・カンパニーZEROという本の中に、企業や経営者がどうやって成長していくかという話があります。

www.amazon.co.jp

この本の中で「運の利益率」という話が登場します。「偉大な企業の中で多かれ少なかれ運に恵まれた企業はあったものの、 "運だけ" で成長したという企業は存在しなかった」という話が出てきます。言い換えると「運という要素をどう活かしたか」が経営ひいては企業の成長に欠かせない要素であることを挙げています。

この事を運の利益率という言葉で語っていました。これは個人においても同じことが言えるのではないでしょうか。要は恵まれた運をどうやって活かすか、運の利益率を挙げていくには結局努力は必要なのです。 これらの本は努力を否定しているのではなく、むしろ推奨していて、ただし分断を産まないようにしないといけないのです。

まとめ

運か実力かといった議論が若干的を外した感じでお互い議論しているのを見てきました。言いたいことは結局シンプルで、努力はしないといけないけど、成功したとしても運に恵まれたことを感謝するべきという話ですね。 昨今の親ガチャなどにも通じる話かもしれません。親が良かった・悪かったというのが運の要素によって左右されていることをガチャというソーシャルゲームの要素で表した皮肉な意見ですが、分断が産まれている現状においてはこういった話も大いに有り得そうに思えます。

自分は「努力は努力できる機会に恵まれた人の権利であり、最大限に利用できる範囲で利用する。そのおかげで成功したとしても自分の努力のおかげとはみなさず、運の要素によってもたらされたものである」という考え方を持っていこうと思っています。