Honov5 RCを触ってみる

〜 v4 と並べて、何が壊れるのか確かめた 〜

田中博悠 / tanahiro2010

田中博悠 / tanahiro2010

  • 三田学園高等学校 1年
  • Hono Conference 2026 コアスタッフ
  • 個人開発でずっと Hono を使ってる

Hono、好きです

tanahiro2010

Hono v5 RC、出ましたね

v5 RC

honojs/hono releases

npm i hono@next

もう触れます

で

あなたのアプリ、

v5 にしたら壊れますか?

⋯

僕のは壊れました

方針

Migration Guide の翻訳はしない

やったこと

「ドキュメントにこう書いてある」

↓

「実際にこうなった」

環境

v4[email protected]
v5[email protected]
Node.jsv24.22.0
TypeScript7.0.2

Apple M4 / 2026-10-18 時点

型

c.req.json()

app.post('/users', async (c) => {
  const body = await c.req.json()

  return c.json({ name: body.name.toUpperCase() })
})

v4 では、このコードが通る

v5

error TS18046: 'body' is of type 'unknown'.

ちゃんと止まった

もうひとつ気になってたのが

c.req.query()

const { name } = c.req.query()
const upper: string = name.toUpperCase()

これ、分割代入してるコードが

全部赤くなるのでは?

v5

error TS18048: 'name' is possibly 'undefined'.
error TS2322: Type 'string[] | undefined' is not
  assignable to type 'string[]'.

全部赤くなった

でも、ここで面白いことに気づいた

実行時の値を比べてみる

GET /q?a=1&a=2

v4v5
query(){"a":"1"}{"a":"1"}
queries(){"a":["1","2"]}{"a":["1","2"]}

完全に同じ

わかったこと

赤くなった箇所は、

v4 の時点でバグり得た箇所

エラー

ここが v5 で一番面白い

v4 の onError()

app.onError((err, c) => {
  console.error(err.message)
  return c.json({ error: 'Internal Server Error' }, 500)
})

v5 の onError()

app.onError('/api/*', async (c, next) => {
  console.error(c.error?.message)
  c.header('X-Error-Handler', 'api')
  await next()
})

app.onError((c) => c.json({ error: 'Error' }, 500))

設計意図

エラー処理を Hono のルーティングシステムの上に
モデル化する

予約メソッドで Router に登録されている

honojs/hono#5541

本当にパスで絞れるのか?

動かしてみた

リクエストヘッダstatus
/api/errorapi500
/other/errorなし500

効いた

ちなみに v4 で同じことを書くと

v4

this.errorHandler is not a function

ここからが本番

onError() を 2回登録したら

どうなる?

実験

app.onError((c) => { order.push('first');  ... })
app.onError((c) => { order.push('second'); ... })

結果

実行されたハンドラ
v4["second"]
v5["first", "second"]

v4 は上書き

v5 は合成

敗因になりそうなところ

型エラーにならない

シグネチャさえ直せば tsc は通る

じゃあ v4 の書き方のまま

動かしたらどうなる?

型チェックは止めてくれる

error TS2339: Property 'message' does not exist
  on type 'Context<BlankEnv, string, any>'.
error TS2339: Property 'json' does not exist
  on type 'Next'.

問題は、これを握りつぶしたとき

実行すると

Context is not finalized.
Did you forget to return a Response object
or `await next()`?

原因を指していない

何が起きてるか

型エラーを消すと、デバッグが遠回りになる

非 Error を throw したら

v4v5
throw 'oops'外に漏れるmessage / cause に 'oops'
throw { code: 42 }外に漏れるmessage は 空

地雷

型でも捕まらないやつ

c.json(undefined)

v4v5
c.json(undefined)200 / 空ボディ500
c.json(() => 1)200 / 空500
TypeError: Value is not JSON serializable

型エラーにならない

どこで踏むか

const user = await db.query.users.findFirst({ ... })
return c.json(user) // 見つからなければ undefined

つまり

v5 で 500 が出る箇所は

v4 の時点でバグっていた箇所

ルーター

ワイルドカードが統一された

v4 は Router ごとにバラバラだった

v4

ルートパスSmartRegExpTriePattern
/x*/y/x/y❌❌❌❌
/x*/y/xz/y✅✅❌❌
/x*y/xay✅✅❌❌
/wild/*/card/wild//card❌❌✅❌

6ケース中3ケースで割れている

v5

ルートパスSmartRegExpTriePattern
/x*/y/x/y✅✅✅✅
/x*/y/xz/y✅✅✅✅
/x*y/xay❌❌❌❌
/wild/*/card/wild//card❌❌❌❌

4つとも一致

ここが一番おいしい

競合するルートを書いてみる

app.get('/x*/y', (c) => c.text('a'))
app.get('/x/*',  (c) => c.text('b'))

v5 の RegExpRouter

UnsupportedPathError: /x/*

じゃあ SmartRouter はどうなる?

選ばれた Router

登録したルートRouter
/x/* だけSmartRouter + RegExpRouter
/x*/y + /x/*SmartRouter + TrieRouter

ルートを1行足しただけで

Router が入れ替わった

性能も変わるのでは?

測った

GET /users/:id × 50,000回 × 12ラウンド

中央値最小
RegExpRouter138.68 ms132.35 ms
Trie フォールバック148.57 ms146.25 ms
差+7.1%+10.5%

遅くなった

でも問題はそこじゃない

しかも

マッチするハンドラも変わる

同じ競合ルートに /x/y を投げると

v4v5
/x/* の方/x*/y の方

性能

v5、速くなったの?

測った

GET /users/:id × 100,000回 × 10ラウンド

中央値最小
v4270.53 ms258.31 ms
v5266.50 ms255.59 ms

app.fetch() のインプロセス計測。実サーバーの性能ではない

誤差

分布が重なっている

ここで、恥ずかしい話を

敗因

ラウンド数を変えただけで

結論が倍違った

わかったこと

ラウンド数と分布を出す

3.3% → 7.1% に変わった

コアの性能は据え置き

v5 の変更は、性能のためじゃない

サイズ

「アダプタ分離で軽くなった」

本当?

インストールサイズ

展開後ファイル数
v41.23 MB574
v50.66 MB310

npm registry の unpackedSize

-46%

3つの測り方が一致した

ファイル数 574 → 310 は完全一致

じゃあバンドルは?

バンドルサイズ

v4v5差
コアのみ18,528 B18,830 B+302 B
アダプタ込み22,760 B22,923 B+163 B

増えた

考えてみれば当たり前

「軽くなった」は

インストールサイズの話

設計

ここまで全部、同じ方向を向いてる

v5 = AIのためのHono

抽象化されていて、選択肢が少ない

Yusuke Wada / Hono Conference 2026

なぜ型を厳しくするのか

それが一番出てるのが

defineHandler()

app.get('/ping', defineHandler(() => ({ pong: true })))
ping -> 200 {"pong":true}

検証も1か所で書ける

app.post('/users', defineHandler({ json: NameSchema })(
  (c, input) => ({ name: input.json.name.toUpperCase() })
))
400 {"error":"Validation failed","issues":[...]}

ハマった

400 ではなく 500 になる

型定義のコメント

A JSON Response by c.json() is not accepted,
because it bypasses the response validation.

やり方が複数あって、どれでも動く

↓

正しいやり方が1つで、他は型で止まる

まとめ

移行チェックリスト

①まず tsc --strict を通す

→ 機械的に潰せる

移行チェックリスト

②型では捕まらない3つ

→ ここが本番

移行チェックリスト

③import を書き換える

まとめ

v5 で壊れるのは

v4 が緩かったところ

壊れたんじゃなくて、

隠れてたバグが出てきただけ

本番投入は RC なので待ちます

でも hono@next で1回ビルドするのは

今やる価値がある

型エラーの量を見れば、

移行コストがその場で分かる

ありがとうございました

One more thing

正式リリースまでの予想

ひっくり返るとしたら

defineHandler() だと思う

一番詰まるのは

hono/cloudflare-pages の削除

外れるかもしれません

正式版が出たら、答え合わせします

npm i hono@next

hono.dev/docs/guides/migrating-to-v5

← → 移動 ・ f フルスクリーン ・ o 一覧 ・ p 発表者
記事版を読む →