encodeURIComponentがURIErrorを投げる理由:孤立サロゲート
絵文字の途中でsliceすると、encodeURIComponentはURIErrorを投げ、TextEncoderはEF BF BDを書き出します。孤立サロゲートの正体と、isWellFormed・toWellFormedの使い方をECMA-262とUnicode 17.0に沿って整理しました。
ユーザー名を短くして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()を確かめておけば、例外と黙った置き換えのどちらを起こすかをコードの側で決められます。
参考
- ECMAScript 2026、19.2.6 URI Handling Functions:19.2.6.3–19.2.6.5
- ECMAScript 2026、22.1 String Objects:22.1.3.10、22.1.3.22、22.1.3.31
- ECMAScript 2026、7.2.7 IsStringWellFormedUnicode
- WHATWG Web IDL:2.13.19 USVString
- WHATWG Encoding Standard:7.4 TextEncoder、7.6 TextEncoderStream
- WHATWG Infra Standard:4.6 Code points、4.7 Strings
- Unicode標準 17.0、第3章:3.8 Surrogates、3.9 Unicode Encoding Forms、表3-5
- RFC 3629, UTF-8, a transformation format of ISO 10646:3節
リンクと内容は2026年10月8日に確認しました。