btoa と atob:Latin-1 専用
btoa(binary to ASCII)は、各文字を 1 バイトとして扱って文字列をエンコードします。atob はその逆です。ASCII なら問題なく動きます。
btoa("hello"); // "aGVsbG8="
atob("aGVsbG8="); // "hello"
ところが、コードポイント 0〜255 の外にある文字では失敗します。
btoa("héllo ✓");
// InvalidCharacterError: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
さらに厄介なことに、Latin-1 に収まる文字はエラーにならず、誤ったバイト列としてエンコードされます。btoa("héllo") は aOlsbG8=(Latin-1 の é = 0xE9)を返しますが、他のすべての言語や Base64 エンコーダー は UTF-8 版の aMOpbGxv を出力します。
btoa と atob は Node.js 16 以降にもレガシーなグローバル関数として存在しますが、サーバー側では Buffer API のほうが高速で、base64url エンコーディングも組み込まれています。Node.js での Base64 をご覧ください。
UTF-8 文字列のエンコードとデコード
どこでも使える解決策は、TextEncoder で文字列を UTF-8 のバイト列に変換し、そのバイト列を「バイナリ文字列」として btoa に渡すことです。
function encodeUtf8(str) {
const bytes = new TextEncoder().encode(str);
let bin = "";
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin);
}
function decodeUtf8(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
encodeUtf8("héllo wörld"); // "aMOpbGxvIHfDtnJsZA=="
decodeUtf8("aMOpbGxvIHfDtnJsZA=="); // "héllo wörld"
atob("aMOpbGxvIHfDtnJsZA=="); // "héllo wörld" (no TextDecoder)
昔ながらの btoa(unescape(encodeURIComponent(str))) という技でも同じ結果になりますが、非推奨の unescape に依存しています。
Uint8Array.toBase64 と fromBase64
新しい API はバイナリ文字列を経由せず、バイト列を直接扱います。Chrome と Edge 140 以降、Firefox 133 以降、Safari 18.2 以降で対応しています。
const bytes = new TextEncoder().encode("héllo wörld");
bytes.toBase64(); // "aMOpbGxvIHfDtnJsZA=="
new TextDecoder().decode(Uint8Array.fromBase64("aMOpbGxvIHfDtnJsZA=="));
// "héllo wörld"
既存のバッファにデコードし、どれだけ使ったかを報告する setFromBase64 もあります。
const target = new Uint8Array(8);
target.setFromBase64("aGVsbG8K"); // { read: 8, written: 6 }
使う前に typeof Uint8Array.prototype.toBase64 === "function" で機能検出してください。すべてのオプションは Uint8Array.toBase64 の解説 で取り上げています。
パディングあり・なしの URLセーフ Base64
新しい API ではオプションを渡します。
const d = new TextEncoder().encode("<<???>>");
d.toBase64({ alphabet: "base64url" }); // "PDw_Pz8-Pg=="
d.toBase64({ alphabet: "base64url", omitPadding: true }); // "PDw_Pz8-Pg"
Uint8Array.fromBase64("PDw_Pz8-Pg", { alphabet: "base64url" }); // bytes of "<<???>>"
btoa/atob では、アルファベットを自分で置き換えます。
const toB64Url = b64 =>
b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const fromB64Url = s => {
s = s.replace(/-/g, "+").replace(/_/g, "/");
return s + "=".repeat((4 - s.length % 4) % 4);
};
toB64Url(btoa("<<???>>")); // "PDw_Pz8-Pg"
atob(fromB64Url("PDw_Pz8-Pg")); // "<<???>>"
これは JWT で使われる形式です。Base64 と Base64URL の違い をご覧いただくか、トークンを JWT デコーダー に貼り付けてみてください。
ファイル、Blob、大きなデータ
ユーザーが選んだファイルを読むなら、FileReader.readAsDataURL が Data URI を返します。プレーンな Base64 が欲しければ、カンマまでを取り除いてください。
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result; // "data:text/plain;base64,aGVsbG8K"
const b64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
};
reader.readAsDataURL(file);
新しい API ならもっと短く、new Uint8Array(await file.arrayBuffer()).toBase64() で済みます。
Base64 をダウンロード可能なファイルに戻すには、バイト列にデコードして Blob で包みます。
const bytes = Uint8Array.fromBase64(b64);
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
大きなバッファを String.fromCharCode(...bytes) のようにスプレッド構文で展開してはいけません。500,000 バイトで Chromium は RangeError: Maximum call stack size exceeded をスローします。代わりに、スライスごとにバイナリ文字列を組み立ててください。
function bytesToBase64(bytes) {
let bin = "";
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// 5,000,000 bytes -> 6,666,668 characters
出力は入力より約 3 分の 1 大きくなります。正確な式は サイズ増加の解説 にあります。
検証とエラーメッセージ
atob は ASCII の空白を無視し、パディングの不足も許容するので、atob(" aG k= ") と atob("aGk") はどちらも "hi" を返します。標準アルファベット外の文字は拒否します。
atob("PDw_Pz8-Pg");
// InvalidCharacterError: Failed to execute 'atob' on 'Window':
// The string to be decoded is not correctly encoded.
fromBase64 は SyntaxError をスローし、lastChunkHandling でより厳格にできます。
Uint8Array.fromBase64("PDw_Pz8-Pg");
// SyntaxError: Found a character that cannot be part of a valid base64 string.
Uint8Array.fromBase64("aGl="); // 2 bytes, accepted
Uint8Array.fromBase64("aGl=", { lastChunkHandling: "strict" });
// SyntaxError: The base64 input terminates with non-zero padding bits.
デフォルトの "loose" はパディングの不足を許容します。"strict" は正しいパディングと 0 のパディングビットを要求します。"stop-before-partial" は、ストリーム処理のデコーダー向けに、不完全な最後のチャンクを残します。メッセージの文言はブラウザによって異なりますが、エラー名は標準化されています。目で確認したいときは、Base64 バリデーター が問題箇所を正確に強調表示します。原因の一覧は よくある Base64 のエラー にあります。
よくある質問
btoa が InvalidCharacterError をスローするのはなぜですか?
btoa が受け付けるのは Latin-1 の範囲(コードポイント 0〜255)の文字だけです。絵文字、キリル文字、日本語を含む CJK の文字が 1 つでもあるとスローします。先に TextEncoder で文字列を UTF-8 のバイト列に変換するか、Uint8Array.prototype.toBase64() を使ってください。
Uint8Array.toBase64 は本番環境で使えますか?
現行の Chrome、Edge、Firefox、Safari には搭載されていますが、古いブラウザや Node.js 24 にはありません。typeof Uint8Array.prototype.toBase64 === "function" で機能検出し、btoa を使うヘルパーにフォールバックしてください。詳しくは Uint8Array.toBase64 の解説 をご覧ください。
atob の結果が é ではなく é のように文字化けするのはなぜですか?
atob は 1 バイトにつき 1 文字を返すため、2 バイトの UTF-8 シーケンスが Latin-1 の 2 文字になってしまいます。結果を Uint8Array.from(bin, c => c.charCodeAt(0)) と TextDecoder に通せば、正しいテキストが得られます。
atob で Base64URL をデコードするには?
- を + に、_ を / に置き換え、長さが 4 の倍数になるまで = でパディングします。新しい API なら Uint8Array.fromBase64(s, { alphabet: "base64url" }) を呼ぶだけで、どちらの手順も不要です。