JavaScript File Handling Table
| Piece | What it does | Field note |
|---|---|---|
file_handlers (manifest) | Declare your types | action URL + accept map of MIME to extensions - puts you in the OS Open With menu |
launch_handler.client_mode | Route the launch | focus-existing reuses the running window; omit it and every double-click spawns a clone |
launchQueue.setConsumer(fn) | Receive launches | Register at STARTUP - launches may arrive while your page loads; the queue buffers within the session |
launchParams.files | The handles | Array of FileSystemFileHandles - the same type File System Access produces, empty for plain navigation |
handle.getFile() | Read the file | Permission-prompted once per handle, then your existing FSA code runs unchanged |
multiple files | One launch, many handles | Shift-click delivers an array; some managers enqueue separate launches - make handling idempotent |
installed PWA only | The gate | Chromium-desktop today (Windows first); browser tabs, iOS and Android never receive launches |
input + drag-drop fallback | The universal path | File Handling ADDS the double-click door - it never replaces in-page selection |
File Handling is what turns a PWA into the double-click target for its own file types: the manifest declares file_handlers (extensions plus MIME types), the OS lists your app in Open With, and when the user opens a file with you, Windows hands it over through launchQueue.setConsumer - your code receives FileSystemFileHandles, the SAME objects File System Access produces.
Bottom line: the launch may happen when your app is CLOSED, so the consumer must be registered at startup - launchQueue queues launch params until a consumer appears, but only within the session the OS chose to start. The handle you get is permission-gated like any File System Access handle: call getFile() and the browser prompts once per handle.
The honest part: this is Chromium-desktop-only today (Windows foremost, installed PWAs only) - no iOS, no Android, no generic browser tab. The production pattern is capability detection plus fallback: if (launchQueue) wire the consumer; always keep your input-type and drag-drop paths, because most of your users will still arrive through them.
How to use
- Declare handlers in the manifest: file_handlers: [{ action: '/open', accept: { 'text/markdown': ['.md', '.markdown'] } }] - action is the URL the OS targets; accept maps MIME types to extensions.
- Add the launch route: launch_handler: { client_mode: 'focus-existing' } in the manifest - route launches into your running window instead of spawning a new one per file.
- Consume the queue at startup: if (window.launchQueue) launchQueue.setConsumer(params => handleFiles(params.files)) - params.files is an array of FileSystemFileHandles, empty for plain navigations.
- Read the files: for each handle, const file = await handle.getFile(); openInEditor(file) - the first getFile() per handle triggers the standard permission prompt.
- Keep the fallbacks: input type=file and drag-and-drop still serve every browser - File Handling ADDS the OS double-click path, it does not replace in-page selection.
Frequently asked questions
Why does my launchQueue consumer never fire?
Three gates in order: the app must be INSTALLED (a regular browser tab never receives OS file launches), the OS association must point at your action URL (check windows Open With after install), and the consumer must be registered in the session the OS launched - if your startup code sets the consumer late, early-arriving launches can be missed on some platforms. Debug with about://apps-internals style tooling or a temporary console log at the very top of your entry script; if setConsumer is set before anything else and the app is installed, launches arrive.
What is the difference between File Handling and File System Access?
File System Access is how the PAGE gets files: showOpenFilePicker, drag-drop, handles in IndexedDB. File Handling is how the OPERATING SYSTEM hands files to your installed app: manifest declaration, OS Open With menu, launchQueue delivery. They meet at the same object type - the launch delivers FileSystemFileHandles that your existing FSA code consumes unchanged - which is why the two ship together in architecture terms: FSA is the in-app plumbing, File Handling is the front door.
What does client_mode do in launch_handler?
It decides where a launch lands: focus-existing routes the file into your already-running window (the behavior users expect from desktop editors), navigate-new opens a fresh client, and match-original-or-navigations tries the launching client first. Without it, every double-click can spawn another window - the classic multi-window mess. navigationFocus is the complementary concept: your action page renders, then your JS reads launchQueue and shows the right view state.
How do multiple files arrive and how should I handle them?
params.files is an array - shift-clicking several files in Explorer delivers all handles in one launch, and some file managers enqueue separate launches instead, so treat both orders as possible: batch the array, and make handleFiles idempotent (opening the same file twice should focus its tab, not duplicate it). Practical shape: collect handles, filter by extension you actually support, then open a tab per file up to a sane cap; queue beyond it.