Reading Files with FileReader and ArrayBuffer
For new code, read a fileβs bytes with await blob.arrayBuffer() (or .text()), reach for the callback-based FileReader only when you need granular progress events, and switch to blob.stream() once files grow large enough that buffering the whole thing in memory becomes a risk.
This article is part of the File API and Blob objects topic inside upload fundamentals and browser APIs. It covers how to turn a File β however you obtained it β into usable bytes.
When to use this approach
- You need a fileβs contents in memory as an
ArrayBuffer,Uint8Array, or string (hashing, parsing headers, image decoding). - You want a live progress bar while reading a large file from disk β
FileReaderexposesprogressevents that the promise-basedBlobmethods do not. - You are processing files too large to hold in RAM and want to stream them through a transform instead.
Prerequisites
- A
FileorBlobreference β from an<input>, a drag-and-drop drop zone, orfetch. - TypeScript with
lib: ["DOM", "DOM.Iterable", "ES2022"]. - A modern browser;
Blob.arrayBuffer(),.text(), and.stream()are supported across Chromium, Firefox, and Safari.
Implementation
The three reading paths solve different problems. The promise-based Blob methods are the default. FileReader adds progress callbacks. stream() avoids holding the whole file at once.
// 1. Default: promise-based, no event plumbing.
async function readBytes(file: Blob): Promise<Uint8Array> {
const buffer: ArrayBuffer = await file.arrayBuffer();
return new Uint8Array(buffer);
}
async function readText(file: Blob): Promise<string> {
return file.text(); // decodes as UTF-8
}
// 2. FileReader: use ONLY when you need progress events.
function readWithProgress(
file: Blob,
onProgress: (fraction: number) => void,
): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as ArrayBuffer);
reader.onerror = () => reject(reader.error ?? new Error("read failed"));
reader.onprogress = (event: ProgressEvent<FileReader>) => {
if (event.lengthComputable) onProgress(event.loaded / event.total);
};
reader.readAsArrayBuffer(file);
});
}
// 3. Stream: process a large file chunk by chunk, never holding it all.
async function hashStreamed(file: Blob): Promise<string> {
const reader = file.stream().getReader();
const chunks: Uint8Array[] = [];
let total = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
total += value.byteLength;
}
// Concatenate only the parts you keep; here we hash incrementally instead.
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.byteLength;
}
const digest = await crypto.subtle.digest("SHA-256", merged);
return [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
Usage:
const input = document.querySelector<HTMLInputElement>("#file")!;
input.addEventListener("change", async () => {
const file = input.files?.[0];
if (!file) return;
const bytes = await readBytes(file);
console.log("read", bytes.byteLength, "bytes; first 4:", [...bytes.slice(0, 4)]);
await readWithProgress(file, (f) => console.log(`reading ${(f * 100).toFixed(0)}%`));
});
Line-by-line on the critical parts
blob.arrayBuffer()resolves to anArrayBuffer; wrap it in aUint8Arrayto index individual bytes (useful for checking magic numbers like0xFF 0xD8for JPEG).FileReader.readAsArrayBuffer()is asynchronous despite the imperative call β results arrive ononload, andreader.resultisnulluntil then. Always read it inside the handler.event.lengthComputableguards the progress math; when false,totalis0and dividing yieldsNaN. Only update the bar when itβs true.blob.stream()returns aReadableStream<Uint8Array>; reading it incrementally means you never allocate one buffer the size of the whole file β essential past a few hundred megabytes.
The promise methods are syntactic wrappers over the same underlying read FileReader performs; choose FileReader purely for its progress event surface. When the file is genuinely large, prefer the stream so you can pipe it straight into an upload β pair this with slicing large files with Blob.slice and send the parts as multipart form data.
Configuration gotchas
Reading reader.result before onload returns null. FileReader is asynchronous; the imperative readAsArrayBuffer() only kicks off the read. Accessing reader.result synchronously after the call yields null. Read it inside onload.
Progress math produces NaN. When event.lengthComputable is false, event.total is 0. Guard every progress calculation with if (event.lengthComputable) to avoid a NaN width on your bar.
readAsDataURL inflates binary by ~33%. Using readAsDataURL to upload media base64-encodes it, adding roughly a third to the payload. For transmission, keep the binary File/Blob β see base64 vs binary encoding. Use data URLs only for inline previews.
Buffering a huge file crashes mobile tabs. arrayBuffer() on a 1 GB file allocates 1 GB; iOS Safari kills tabs over ~1 GB heap. Use stream() and process chunks for anything large.
Testing / verification
Assert the byte count and a known magic number without a server:
async function verifyJpeg(file: File): Promise<void> {
const bytes = new Uint8Array(await file.arrayBuffer());
console.assert(bytes.byteLength === file.size, "byte length mismatch");
console.assert(
bytes[0] === 0xff && bytes[1] === 0xd8,
"not a JPEG (missing FFD8 SOI marker)",
);
console.log("verified", file.name, bytes.byteLength, "bytes");
}
FAQ
Should I use FileReader or Blob.arrayBuffer() in new code?
Use await blob.arrayBuffer() (or .text()) by default β it is shorter and promise-based. Reach for FileReader only when you need its progress events to drive a live reading indicator.
Why is FileReader.result null right after readAsArrayBuffer?
The read is asynchronous. readAsArrayBuffer() merely starts it; the bytes appear in reader.result when the onload event fires. Read the result inside that handler, never immediately after the call.
How do I read a file thatβs too big to fit in memory?
Call blob.stream() and consume the ReadableStream chunk by chunk, processing or uploading each chunk before discarding it. This keeps peak memory flat regardless of file size.
When should I use readAsDataURL?
Only for inline previews (e.g. an <img src> thumbnail). For uploading, data URLs base64-encode the file and add about 33% overhead, so send the raw binary instead.