browser-fs-access papers over the save gap, and that gap is the exception model
File System Access API with legacy fallback in the browser
At a glance
- What is it?
- browser-fs-access is a ponyfill that exposes fileOpen, directoryOpen and fileSave over the File System Access API with a fallback to input type=file and a download anchor. The functions look identical on both paths, but the fallback cannot throw, so error handling is where a file on one browser becomes broken code on another.
- Who is it for?
- Adopt browser-fs-access if you want one set of fileOpen, directoryOpen and fileSave calls that work on browsers with the File System Access API and degrade to the legacy pickers elsewhere. Do not adopt it if your app depends on catching errors from a failed save, because the legacy download path cannot raise and the difference is handled through the legacySetup parameter documented in index.d.ts.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 123 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One call, two implementations
The library is a ponyfill, not a polyfill, which is the distinction that matters most. It never patches the browser. Instead it exports its own functions and, at runtime, uses the native File System Access API where it exists and falls back to the older mechanisms where it does not.
The fallback targets are specific: the input element with type=file for opening, and an anchor with a download attribute for saving. Those are the two things the web platform could do before the File System Access API arrived.
The feature detection is exposed rather than hidden. The module detects support and loads only the code that is relevant, and it exports a supported flag so your own code can branch:
if (supported) {
console.log('Using the File System Access API.');
} else {
console.log('Using the fallback implementation.');
}That matters for more than logging. In browsers without the API, features that only exist in the native path, notably saving back to an existing file handle, simply do not work, and knowing which path you are on explains the difference.
Installation and the two build outputs
The package is published to npm as browser-fs-access:
npm install --save browser-fs-accessIt is an ES module package with a CommonJS fallback, and the exports map is explicit about which artefact each environment gets:
"exports": {
".": {
"types": "./index.d.ts",
"module": "./dist/index.modern.js",
"import": "./dist/index.modern.js",
"require": "./dist/index.cjs",
"browser": "./dist/index.modern.js"
},
"./package.json": "./package.json"
}So a bundler using the browser condition and an ESM import both get index.modern.js, while require resolves to index.cjs. The build is done by microbundle with the modern and cjs formats and no sourcemap and no generated types, the latter being handled by the hand-written index.d.ts shipped alongside.
The repo also holds a demo served with npx http-server against the /demo/ directory, containing index.html, script.mjs and image-to-blob.mjs, which is the fastest way to see both code paths side by side.
Options for opening: accept lists, startIn, and remembering directories
fileOpen takes an options object, and an array of them, which is how a picker offers several filter groups. The defaults are documented in the API section, which is worth reading because the differences between groups are subtle.
mimeTypes defaults to */*, extensions defaults to an empty string, multiple defaults to false, and description defaults to an empty string. excludeAcceptAllOption defaults to false, and setting it to true removes the option to lift the filter in the picker.
Two options go beyond filtering. startIn names a well-known directory, or takes a file or directory handle, to suggest where the picker opens. And id lets the user agent remember a different directory for different identifiers, so a picker with id 'projects' can reopen where it was rather than resetting to the last used folder.
Passing an array of option groups gives you multiple labelled entries in the picker, one for images and one for text, each with its own mimeTypes and extensions:
const blobs = await fileOpen([
{
description: 'Image files',
mimeTypes: ['image/jpg', 'image/png', 'image/gif', 'image/webp'],
extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
multiple: true,
},
{
description: 'Text files',
mimeTypes: ['text/*'],
extensions: ['.txt'],
},
]);directoryOpen shares startIn and id, adds recursive which defaults to false, mode which is read or readwrite and defaults to read, and skipDirectory, a callback receiving an entry and returning true to skip it. The skipDirectory callback is what keeps dotfile directories out of a recursive walk.
fileSave accepts a Blob, a Promise or a Response, which is the ergonomic win
The save side is where the library does the most work for you. fileSave takes a Blob, a Promise of a Blob, or a Response, and the last two are streamed rather than buffered.
A Response can be passed straight through without touching the body:
const response = await fetch('foo.png');
await fileSave(response, {
fileName: 'foo.png',
extensions: ['.png'],
});And a promise for a blob that may take a while does not need awaiting first, which is what makes it usable for large generated files:
const blob = createBlobAsyncWhichMightTakeLonger(someData);
await fileSave(blob, {
fileName: 'Untitled.png',
extensions: ['.png'],
});The options are fileName and extensions as suggestions, plus startIn and id as before. Two extra parameters carry the native-only capability: an existing handle to save back to, obtained from the handle property of a Blob previously returned by fileOpen, which the README flags as non-standard; and throwIfExistingHandleNotGood, defaulting to false, which decides whether to throw when that handle has gone stale, for example because the underlying file was deleted, rather than opening a new save dialog.
The exception gap is the reason to read index.d.ts
Here is the honest limitation. The File System Access API supports exceptions, so an application can throw when something goes wrong: permission not granted, out of disk space, or the user cancelling the dialog. The legacy save method does not support exceptions.
That is not a small difference. A call that can throw forces a try and catch and therefore forces you to decide what a cancelled save means; a call that cannot throw returns quietly, and a user who hit cancel may look like a user whose write succeeded.
The README's answer is to look at index.d.ts for the documentation of the legacySetup parameter, which is how the fallback is configured. So the resolution of this gap is in the TypeScript declaration file rather than in the prose documentation, which is worth knowing before you go looking for a runtime option that does not exist.
There is also a small compatibility touch: the module polyfills webkitRelativePath on returned files so the property is present and consistent whichever implementation ran, which stops downstream code from having to branch on the implementation.
Proven in Excalidraw and SVGcode, narrower than native-file-system-adapter
Two applications are named as using the module. Excalidraw is a drawing app, while SVGcode converts raster images to SVGs.
That is the strongest evidence available about fitness, more than any feature list, because both applications care about a property that is hard: consistent behaviour across browsers with and without the API. An image converter that silently loses the user's file on one browser is not a usable product.
The README also names an alternative, native-file-system-adapter by jimmywarting, described as similar but more extensive. The README describes native-file-system-adapter as similar but more extensive.
The repository is not archived, the last push was on 2026-06-22, and releases are v0.36.0, v0.37.0 and v0.38.0 from April and June 2025. The Apache-2.0 licence covers both the source and the demo.
Editorial conclusion
Adopt browser-fs-access if you want one set of fileOpen, directoryOpen and fileSave calls that work on browsers with the File System Access API and degrade to the legacy pickers elsewhere. Do not adopt it if your app depends on catching errors from a failed save, because the legacy download path cannot raise and the difference is handled through the legacySetup parameter documented in index.d.ts. First check the exported bundle for your build target, since the package ships modern and CommonJS outputs through microbundle and the resolution differs by environment.
Frequently asked questions
What does browser-fs-access do when the File System Access API is unavailable?
It falls back to the legacy mechanisms: input elements with type=file for opening and an anchor with a download attribute for saving. The exported supported flag tells you which path is in use.
Is browser-fs-access a polyfill or a ponyfill?
A ponyfill. It does not patch the browser with missing methods; it exports its own fileOpen, directoryOpen and fileSave functions and picks the underlying implementation at runtime.
Why can my app not catch an error when saving a file?
The legacy save method does not support exceptions, so on browsers without the File System Access API a save cannot throw. The README points to the legacySetup parameter documented in index.d.ts for how the fallback is configured.
Which real applications use browser-fs-access?
The README names the Excalidraw drawing app and SVGcode, which converts raster images into SVGs.
Can fileSave take a Response or a Promise of a Blob?
Yes. fileSave accepts a Blob, a Promise of a Blob or a Response, and the promise and response cases are streamed rather than buffered, so no await is needed before passing a slow blob.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/googlechromelabs-browser-fs-access)Was this useful?