encodeURIComponentがURIErrorを投げる理由:孤立サロゲート

絵文字の途中でsliceすると、encodeURIComponentはURIErrorを投げ、TextEncoderはEF BF BDを書き出します。孤立サロゲートの正体と、isWellFormed・toWellFormedの使い方をECMA-262とUnicode 17.0に沿って整理しました。

開発エンコーディングUnicode

ユーザー名を短くしてURLに入れるコードで、名前の末尾に絵文字があるとこうなります。

const cut = "ab😀".slice(0, 3); // "ab" + "\uD83D"
encodeURIComponent(cut);        // URIError
new TextEncoder().encode(cut);  // 61 62 EF BF BD

同じ文字列なのに、一方は例外を投げ、もう一方は黙ってバイトを置き換えます。原因は、切った位置に残ったペアのないサロゲート(孤立サロゲート)です。

半分だけ残った絵文字

😀(U+1F600)はUTF-16ではD83D DE00の2つのコードユニットに分かれます(Unicode標準 表3-5)。sliceはコードユニット単位で切るので、slice(0, 3)には前半のD83Dだけが残ります。

UnicodeはD800–DFFFをサロゲート用に取り分け、スカラー値から外しています(D71–D76)。ペアのない単独のサロゲートコードユニットは、UTF-16として不正な形式です(D91)。それでもJavaScriptの文字列は16ビット値の並びなので、この状態をそのまま保持できます。

UTF-8には居場所がない

RFC 3629の3節は、U+D800–U+DFFFをUTF-8で符号化することを禁じています。そのため文字列をUTF-8のバイトに変えるAPIは、孤立サロゲートに出会うと、止まるか別の文字に置き換えるしかありません。

encodeURIとencodeURIComponentは止まります。ECMA-262のEncode操作(19.2.6.5)は、コードポイントを読み進めてペアのないサロゲートを見つけるとURIErrorを投げます。URLエンコードの記事で取り上げたdecodeURIComponent('%E3%81')のエラーはバイト不足によるもので、こちらはエンコードする側で起きるエラーです。

TextEncoderは置き換えます。encode()の引数はUSVStringで、Encoding Standardはこの変換で孤立サロゲートがU+FFFD(�)に置き換わると説明しています。U+FFFDはUTF-8でEF BF BDです。元の絵文字は復元できない形で消え、エラーも出ません。

先に確かめて、自分で選ぶ

ECMA-262にはこの状態を扱うメソッドが2つあります。isWellFormed()(22.1.3.10)は孤立サロゲートがあるかを調べ、toWellFormed()(22.1.3.31)はそれをU+FFFDに置き換えた新しい文字列を返します。

cut.isWellFormed();                     // false
encodeURIComponent(cut.toWellFormed()); // "ab%EF%BF%BD"

ただ、�に置き換えるのもデータを失うことなので、そもそもコードユニット単位で切らないほうが安全です。コードポイントや見た目の1文字単位で切る方法は文字数がツールごとに違う理由にまとめました。文字列がバイトとして出ていく境界でisWellFormed()を確かめておけば、例外と黙った置き換えのどちらを起こすかをコードの側で決められます。

参考

リンクと内容は2026年10月8日に確認しました。

ブログへ