Typed Arrays, Blobs & Base64

JavaScript · Lesson 55 of 61 9 min read Updated

Photos, PDFs, audio, ZIP files and many network messages are not text: they are sequences of bytes, numbers from 0 to 255. Ordinary JavaScript strings and arrays are not built for this. For raw binary data, JavaScript has ArrayBuffer and typed arrays, and the browser adds Blob and File for chunks of data such as downloads and uploads.

In this lesson you will create and read bytes, see how text becomes bytes, build files in the browser and let users download them, read files users choose, and convert bytes to base64 and hex, including the new methods from ES2026.

ArrayBuffer and typed arrays

An ArrayBuffer is a fixed-size block of memory, filled with zeros when it is created. You cannot read or write it directly. Instead you look at it through a view. A typed array is a view that treats the bytes as a list of numbers of one type:

Typed arrayEach itemRange
Uint8Array1 byte, unsigned0 to 255
Uint8ClampedArray1 byte, clamped0 to 255 (out-of-range values stick to the limit)
Int16Array, Uint16Array2 bytes−32,768 to 32,767 / 0 to 65,535
Int32Array, Uint32Array4 bytesabout ±2.1 billion / 0 to 4.29 billion
Float32Array, Float64Array4 or 8 bytesdecimals (Float64 is a normal JavaScript number)
BigInt64Array, BigUint64Array8 bytesvery large whole numbers as BigInt

ES2025 added Float16Array, a 2-byte decimal type used in graphics and machine learning; it works in current browsers but not in Node.js 22.

JavaScriptRun it yourself
const buffer = new ArrayBuffer(8); // 8 bytes, all zero
console.log(buffer.byteLength);

const bytes = new Uint8Array(buffer); // view the bytes one at a time
bytes[0] = 200;
bytes[1] = 300; // too big for one byte: wraps around
bytes[2] = -1;  // negative: wraps around too
console.log(bytes);

const pairs = new Uint16Array(buffer); // the same 8 bytes, two at a time
console.log(pairs.length, pairs[0]);

const clamped = new Uint8ClampedArray([300, -20, 127.6]);
console.log(clamped);

const marks = new Float32Array([91.5, 78.25, 0.1]);
console.log(marks, marks.byteLength);
Output
8
Uint8Array(8) [
  200, 44, 255, 0,
    0,  0,   0, 0
]
4 11464
Uint8ClampedArray(3) [ 255, 0, 128 ]
Float32Array(3) [ 91.5, 78.25, 0.10000000149011612 ] 12

Several important behaviours show here:

  • A typed array has a fixed length and a fixed item type. Values that do not fit are not rejected: 300 wraps to 44 (300 − 256) and -1 becomes 255. The clamped version sets them to the nearest limit instead, which is what canvas pixel data uses (see Canvas Graphics).
  • Two views can share one buffer. pairs[0] reads bytes 0 and 1 together: 200 + 44 × 256 = 11464. The low byte comes first because almost all computers are little-endian.
  • Float32Array stores decimals with less precision than normal numbers, so 0.1 comes back slightly changed.

Typed arrays support most array methods: map, filter, slice, includes, forEach, for...of, and the ES2023 toSorted and with. map returns a typed array of the same type, so results are wrapped or rounded to fit. Use Array.from(typed) when you need a normal array. One method to know: subarray(start, end) creates a new view on the same memory, while slice() copies.

DataView: reading binary formats

File formats and network protocols mix different number sizes in one record: two bytes here, four bytes there. A DataView reads and writes any type at any byte position, and lets you choose the byte order (endianness). Big-endian (most significant byte first) is the default and is common in file formats and networks; pass true as the last argument for little-endian.

JavaScriptRun it yourself
// One 8-byte record for a train ticket:
// bytes 0-1 train number, byte 2 coach, byte 3 berth, bytes 4-7 fare in paise
const record = new ArrayBuffer(8);
const view = new DataView(record);

view.setUint16(0, 12951);         // big-endian (the default)
view.setUint8(2, 4);
view.setUint8(3, 33);
view.setUint32(4, 145000, true);  // little-endian

console.log(new Uint8Array(record));
console.log("Train", view.getUint16(0), "coach", view.getUint8(2), "berth", view.getUint8(3));
console.log("Fare ₹" + view.getUint32(4, true) / 100);
console.log("Wrong byte order:", view.getUint32(4));
Output
Uint8Array(8) [
   50, 151, 4, 33,
  104,  54, 2,  0
]
Train 12951 coach 4 berth 33
Fare ₹1450
Wrong byte order: 1748369920

Reading with the wrong byte order gives nonsense, so every binary format's documentation states which one it uses. Also check sizes: a 10-digit PNR number does not fit in 32 bits, and setUint32 would silently keep only the low 32 bits.

Text and bytes: TextEncoder and TextDecoder

Text must be encoded into bytes before it is saved or sent, and almost everything today uses UTF-8. In UTF-8, English letters take one byte each, but ₹ takes three and most Devanagari letters take three. TextEncoder turns a string into UTF-8 bytes and TextDecoder turns bytes back into a string.

JavaScriptRun it yourself
const encoder = new TextEncoder();
for (const text of ["Pune", "₹", "नमस्ते", "Fee: ₹1,299"]) {
  console.log(text, "length", text.length, "bytes", encoder.encode(text).length);
}
console.log(encoder.encode("₹"));

const decoder = new TextDecoder(); // UTF-8 by default
const fromServer = new Uint8Array([0xe0, 0xa4, 0xad, 0xe0, 0xa4, 0xbe, 0xe0, 0xa4, 0xb0, 0xe0, 0xa4, 0xa4]);
console.log(decoder.decode(fromServer));

try {
  new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xe2, 0x82]));
} catch (error) {
  console.log("Broken UTF-8:", error.name);
}
Output
Pune length 4 bytes 4
₹ length 1 bytes 3
नमस्ते length 6 bytes 18
Fee: ₹1,299 length 11 bytes 13
Uint8Array(3) [ 226, 130, 185 ]
भारत
Broken UTF-8: TypeError

The byte count matters whenever a limit is in bytes rather than characters, such as a database column, an SMS, or a network packet. Without fatal: true, invalid bytes are replaced with the � character instead of throwing.

Blob and File

A Blob (binary large object) is an immutable chunk of data with a size and a MIME type, such as "text/csv" or "image/png". Create one from strings, typed arrays or other blobs, then read it back with promise-based methods:

JavaScript
const blob = new Blob(["name,marks\nAarav,91\nPriya,88\n"], { type: "text/csv" });
blob.size;                 // 29 (bytes)
blob.type;                 // "text/csv"
await blob.text();         // the contents as a string
await blob.arrayBuffer();  // the contents as an ArrayBuffer
blob.slice(0, 10);         // a new Blob with the first 10 bytes

A File is a Blob with a name and a lastModified date. You get File objects from <input type="file">, from drag and drop, or by creating them yourself with new File([data], "marks.csv", { type: "text/csv" }).

To let a user download a blob, give it a temporary object URL with URL.createObjectURL(blob) and put that URL in a link with a download attribute. When the link is no longer needed, call URL.revokeObjectURL(url) to free the memory.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Download a CSV</title>
</head>
<body>
  <h1>Class 10 marks</h1>
  <button type="button" id="make">Create marks.csv</button>
  <p id="info"></p>

  <script>
    const students = [
      { name: "Aarav", maths: 91, science: 84 },
      { name: "Priya", maths: 88, science: 95 },
      { name: "Farhan", maths: 76, science: 81 },
    ];

    document.querySelector("#make").addEventListener("click", () => {
      const rows = ["name,maths,science", ...students.map((s) => `${s.name},${s.maths},${s.science}`)];
      const blob = new Blob([rows.join("\n")], { type: "text/csv" });
      const url = URL.createObjectURL(blob);

      const link = document.createElement("a");
      link.href = url;
      link.download = "marks.csv"; // the suggested file name
      link.textContent = `Download marks.csv (${blob.size} bytes)`;

      const info = document.querySelector("#info");
      info.replaceChildren(link);
      link.addEventListener("click", () => {
        setTimeout(() => URL.revokeObjectURL(url), 1000); // free the memory after the download starts
      });
    });
  </script>
</body>
</html>

The same technique saves images from a canvas (canvas.toBlob()), generated PDFs or JSON backups, with no server involved. (Inside the Try-it editor's sandbox, your browser may block the actual download; on a normal page it works.)

Reading files the user chooses

When a user picks a file, JavaScript can read it in the browser before uploading anything. Here the page shows the file's details, its first bytes in hexadecimal, and guesses its real type from those bytes. Most formats begin with fixed magic bytes: PDFs start with the text %PDF, PNG images with byte 137 followed by PNG.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Inspect a file</title>
</head>
<body>
  <label>Choose any file: <input type="file" id="picker"></label>
  <pre id="log"></pre>

  <script>
    const signatures = [
      { type: "PDF document", bytes: [0x25, 0x50, 0x44, 0x46] },
      { type: "PNG image", bytes: [0x89, 0x50, 0x4e, 0x47] },
      { type: "JPEG image", bytes: [0xff, 0xd8, 0xff] },
      { type: "ZIP archive (also .docx, .xlsx)", bytes: [0x50, 0x4b, 0x03, 0x04] },
    ];

    function detect(bytes) {
      const match = signatures.find((sig) => sig.bytes.every((b, i) => bytes[i] === b));
      return match ? match.type : "unknown";
    }

    document.querySelector("#picker").addEventListener("change", async (event) => {
      const file = event.target.files[0];
      if (!file) return;

      const head = new Uint8Array(await file.slice(0, 8).arrayBuffer());
      const hex = Array.from(head, (b) => b.toString(16).padStart(2, "0")).join(" ");

      document.querySelector("#log").textContent = [
        `Name: ${file.name}`,
        `Size: ${file.size.toLocaleString("en-IN")} bytes`,
        `Type reported by the browser: ${file.type || "(none)"}`,
        `First bytes: ${hex}`,
        `Looks like: ${detect(head)}`,
      ].join("\n");
    });
  </script>
</body>
</html>

For a PDF named fee-receipt.pdf, the page shows lines such as:

Output
Name: fee-receipt.pdf
Size: 48,213 bytes
Type reported by the browser: application/pdf
First bytes: 25 50 44 46 2d 31 2e 37
Looks like: PDF document

file.type is only a guess based on the file name, so a file renamed from .exe to .pdf would still say application/pdf. Checking magic bytes is better, but it is a convenience for users, not security: a server must always check uploaded files again.

Base64 and hex

Base64 writes any bytes using only 64 safe characters (A to Z, a to z, 0 to 9, + and /), so binary data can travel inside text: JSON, URLs, emails and data: URLs. It makes data about one third larger. Hex (hexadecimal) writes each byte as two characters from 0 to f, which is common for hashes and colour codes.

The old functions btoa() (bytes to base64) and atob() (base64 to bytes) work on "binary strings" and throw an InvalidCharacterError for any character above code 255, such as ₹ or Hindi text. The workaround was to encode to UTF-8 first: btoa(String.fromCharCode(...new TextEncoder().encode(text))), which is hard to read and fails for large data.

Common mistakes

  • Expecting a typed array to grow. Its length is fixed when created; build a new, larger one and copy with set().
  • Storing values that do not fit. Uint8Array silently wraps 256 to 0. Choose a type with enough range.
  • Using string.length as a byte count. Encode with TextEncoder and check .length of the result.
  • Forgetting the byte order with DataView. Check the format's documentation and pass true for little-endian.
  • Never revoking object URLs. Each one keeps its blob in memory until URL.revokeObjectURL() or the page closes.
  • Using btoa() on Unicode text. Encode to bytes first, or use the ES2026 toBase64().

Key points

  • ArrayBuffer is raw memory; typed arrays such as Uint8Array and Float32Array view it as numbers of one type.
  • Typed arrays have a fixed length and wrap or round values that do not fit.
  • DataView reads mixed number types at any position, in big-endian or little-endian order.
  • TextEncoder and TextDecoder convert between strings and UTF-8 bytes.
  • Blob and File hold chunks of data; read them with text() and arrayBuffer(), and download them with URL.createObjectURL().
  • ES2026 adds toBase64(), fromBase64(), toHex() and fromHex() to Uint8Array, replacing the awkward btoa() workarounds.

Exercise

Write a function byteReport(text) that returns an object with the string length, the UTF-8 byte length, and the bytes written in hex (two characters per byte, separated by spaces). Test it with "Kochi", "₹99" and "भारत". Use TextEncoder, and build the hex with toString(16) and padStart so that it runs everywhere.

Show solution
JavaScript
function byteReport(text) {
  const bytes = new TextEncoder().encode(text);
  const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(" ");
  return { text, characters: text.length, bytes: bytes.length, hex };
}

for (const sample of ["Kochi", "₹99", "भारत"]) {
  console.log(byteReport(sample));
}
Output
{ text: 'Kochi', characters: 5, bytes: 5, hex: '4b 6f 63 68 69' }
{ text: '₹99', characters: 3, bytes: 5, hex: 'e2 82 b9 39 39' }
{
  text: 'भारत',
  characters: 4,
  bytes: 12,
  hex: 'e0 a4 ad e0 a4 be e0 a4 b0 e0 a4 a4'
}

Array.from with a mapping function turns each byte into a two-character hex string; padStart adds the leading zero for bytes below 16. In a browser that supports ES2026 you could write bytes.toHex() instead, but it has no spaces.

Quick Quiz

1. What happens when you store 300 in a Uint8Array?

2. How many UTF-8 bytes does the string "₹" take?

3. Which tool reads a 2-byte number followed by a 4-byte number from the same buffer, with a chosen byte order?

4. Why does btoa("₹500") throw an error?

Learn JavaScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call