Consider the following code:
import { ErrorBoundary } from "react-error-boundary";
import { memo, Suspense, use, useMemo, useState } from "react";
const saveFile = (file: File) => {
const blobUrl = URL.createObjectURL(file);
const anchorElement = document.createElement("a");
anchorElement.href = blobUrl;
anchorElement.download = file.name;
document.body.appendChild(anchorElement);
anchorElement.click();
document.body.removeChild(anchorElement);
URL.revokeObjectURL(blobUrl);
};
// Without memo, Safari iOS either just vaguely says "A problem repeatedly occurred on..."
// even if saveFile isn't used
const FileBytes = memo(
({ bytesPromise }: { bytesPromise: Promise<Uint8Array> }) => {
const bytes = use(bytesPromise);
return <div>{Array.from(bytes).slice(0, 20)}</div>;
},
);
function App() {
const [file, setFile] = useState<File | null>(null);
const bytesPromise = useMemo(
() => (file !== null ? file.bytes() : null),
[file],
);
return (
<>
<input
type="file"
onChange={(e) => {
if (e.target.files !== null) {
const file = Array.from(e.target.files)[0];
if (file !== undefined) {
setFile(file);
}
}
}}
/>
<button
// Errors both when onClick is async and sync
onClick={async () => {
if (file !== null) {
// Pretend I have changed the file in some way
const newFile = new File([file], file.name, {
type: file.type,
lastModified: file.lastModified,
});
setFile(newFile);
saveFile(newFile);
}
}}
>
Save file
</button>
{file !== null && (
<div>
{file.name}
{file.type}
</div>
)}
{bytesPromise !== null && (
<ErrorBoundary
fallbackRender={({ error }) => (
<div>
{error instanceof Error
? JSON.stringify({
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause,
})
: "Unknown error"}
</div>
)}
>
<Suspense fallback={<>Loading...</>}>
<FileBytes bytesPromise={bytesPromise} />
</Suspense>
</ErrorBoundary>
)}
</>
);
}
export default App;
When it runs, on mobile iOS Safari (v. 26.3), it will throw a NotReadableError error with message "The I/O read operation failed." This error does not occur on Firefox, Chrome, or Desktop Safari.
The crux of the error surprisingly is the saveFile call. If you remove it, everything will work perfectly. I am not certain of the reason why.
At first, I assumed the error was due to saveFile somehow invalidating the original Blob, but even with various clone implementations, it still would error. This does include sync implementations using just the new File constructor and/or slice.
// errors
const [newFile, newFile2] = await Promise.all([cloneFile(file), cloneFile(file)];
setFile(newFile);
saveFile(newFile2);
// errors
const newFile = await cloneFile(file);
saveFile(await cloneFile(file));
setFile(newFile);
// works
const newFile = await cloneFile(file);
setFile(newFile);
saveFile(await cloneFile(file));
Given the latter worked, it may seem that setting the file before cloning it again to be saved is the solution but after some testing, I noticed more bizarre behavior in that the following worked.
const newFile = new File([file], file.name, {
type: file.type,
lastModified: file.lastModified,
});
setFile(newFile);
void (await file.bytes());
saveFile(newFile);
I used bytes here but text and arrayBuffer method do work (but stream and slice do not). Given the useless return, it may seem that the problem involves the click handler being asynchronous but the following will also error:
<button
onClick={() => {
if (file !== null) {
// The I/O read operation failed. name NotReadableError
const newFile = new File([file], file.name, {
type: file.type,
lastModified: file.lastModified,
});
setFile(newFile);
saveFile(newFile);
}
}}
>
Save file
</button>
Consider the following code:
When it runs, on mobile iOS Safari (v. 26.3), it will throw a NotReadableError error with message "The I/O read operation failed." This error does not occur on Firefox, Chrome, or Desktop Safari.
The crux of the error surprisingly is the
saveFilecall. If you remove it, everything will work perfectly. I am not certain of the reason why.At first, I assumed the error was due to
saveFilesomehow invalidating the original Blob, but even with various clone implementations, it still would error. This does include sync implementations using just the new File constructor and/or slice.Given the latter worked, it may seem that setting the file before cloning it again to be saved is the solution but after some testing, I noticed more bizarre behavior in that the following worked.
I used
byteshere buttextandarrayBuffermethod do work (butstreamandslicedo not). Given the useless return, it may seem that the problem involves the click handler being asynchronous but the following will also error: