In expo-file-system's File class, how do you write and read file contents, and which of those calls block the JavaScript thread?
answer
- write is synchronous, returns void
- text, base64, bytes return Promises
- every read has a *Sync twin
- copy and move async since SDK 56
- sync native calls run on the JS thread
basics
~20 sexpo-file-system's file.write(content) is synchronous; reads come as text(), base64() and bytes() Promises plus textSync(), base64Sync() and bytesSync(). Synchronous calls run native I/O on the JavaScript thread and block it, so large files need the async reads.
solid answer
~40 sWith `expo-file-system` I create a `File` object such as `new File(Paths.document, 'settings.json')`, which does not touch the disk. `file.write(content, { encoding, append })` takes a string or `Uint8Array`, is synchronous and returns nothing; it creates a missing file, but the parent directory must exist. Reading has async and sync pairs: `text()`, `base64()` and `bytes()` return Promises, `textSync()`, `base64Sync()` and `bytesSync()` return the value, and `json()` parses the text asynchronously. `create()`, `delete()`, `info()`, `exists` and `size` are synchronous too, while `copy()` and `move()` return Promises since SDK 56. A synchronous call runs the native I/O on the JavaScript thread and blocks it until it returns, which is fine for a small settings file but freezes the screen's JavaScript on large files.
code
typescript · 14 linesimport { File, Paths } from 'expo-file-system';
type Settings = { units: 'metric' | 'imperial'; lastRegion?: string };
const settingsFile = new File(Paths.document, 'settings.json');
export function saveSettings(s: Settings): void {
settingsFile.write(JSON.stringify(s)); // synchronous; creates the file if missing
}
export async function loadSettings(): Promise<Settings | null> {
if (!settingsFile.exists) return null;
return (await settingsFile.json()) as Settings; // async read off the JS thread
}go deeper
Recall that you build a File from Paths plus a name, write with write() and read with text() or textSync().
Explain which calls are synchronous, that synchronous native functions run on the JavaScript thread, and that copy and move now return Promises.
Show when sync I/O becomes a jank source, and move large reads to async calls, streams or a FileHandle with chunked reads.
Set team conventions for file I/O: size limits for sync calls, a wrapper that centralises error handling, and review rules for the SDK 56 copy and move change.
## The object API in one paragraph Since SDK 54, `expo-file-system` exports `File`, `Directory` and `Paths`. A `File` is a **reference to a path**, built from segments: `new File(Paths.document, 'notes', 'trip.md')`. Constructing it performs no I/O and succeeds whether or not the file exists; it throws only when the path already holds a directory. The methods on the object then read, write and relocate it. ## Which calls are synchronous | Operation | Synchronous | Returns a Promise | |---|---|---| | Write a string or bytes | `write(content, options)` | none | | Read as text | `textSync()` | `text()` | | Read as base64 | `base64Sync()` | `base64()` | | Read raw bytes | `bytesSync()` | `bytes()`, `arrayBuffer()` | | Parse JSON | none | `json()` | | Copy | `copySync(dest)` | `copy(dest)` | | Move | `moveSync(dest)` | `move(dest)` | | Create, delete, rename, `info()`, `exists`, `size` | yes | none | `write()` accepts a string or a `Uint8Array` and an options object with **`encoding`** (`'utf8'` by default, or `'base64'` to decode base64 text into bytes) and **`append`** (default `false`, which overwrites). It creates the file if it is missing, but not missing parent folders: create the `Directory` first. ## Why "synchronous" means "blocks the JavaScript thread" `expo-file-system` is an Expo module. In the Expo Modules API a native **`Function`** is synchronous: when JavaScript calls it, the native code runs on the same thread and the script waits until it returns. An **`AsyncFunction`** runs its native body on a background queue and hands JavaScript a Promise. `write`, `textSync`, `create` and `delete` are declared as `Function`, and `exists` and `size` are synchronous native properties; `text`, `bytes`, `copy` and `move` are `AsyncFunction`. So the practical rules are: 1. **Small files can use the synchronous calls.** Reading a 2 KB settings file with `textSync()` costs well under a frame and keeps code simple. 2. **Large files need the asynchronous reads.** `textSync()` on a multi-megabyte file keeps the JavaScript thread busy, so touches, state updates and JavaScript-driven animations stall until it returns. 3. **Very large files need streaming.** `file.readableStream()` and `file.writableStream()`, or a `FileHandle` from `file.open(mode)` with `readBytes(n)` and `writeBytes(bytes)`, process a file in chunks instead of loading it whole. 4. **There is no async `write`.** Writing a very large buffer in one call blocks; write in chunks through a handle or stream when size is unbounded. ## The SDK 56 change that breaks old code Before SDK 56, `copy()` and `move()` were synchronous. They now return Promises, and `copySync()` and `moveSync()` keep the old behaviour. Code written earlier, and some documentation snippets, still call `file.copy(backup)` and immediately read `backup`. Without `await` that read can run before the copy finishes. Conversely, `await file.write(...)` is harmless but misleading: `write` returns nothing, so the `await` waits for nothing. ## Checklist for review - `await` on every `text()`, `bytes()`, `json()`, `copy()` and `move()`; - no `await` pretending `write()` is asynchronous; - no `textSync()` or `bytesSync()` on files whose size the app does not control; - parent directories created with `create({ intermediates: true, idempotent: true })` before writing; - reads guarded by `file.exists` or `try/catch` when the file may be missing.
- How do you read only part of a large file with the object API?Open a handle with `file.open()` (read-write by default for `file://` URIs), set `handle.offset` to the byte position, call `readBytes(length)` and then `close()`. `readableStream()` is the alternative when the whole file must be processed in chunks. Both avoid loading the whole file into a string.
- Does the copy() change in SDK 56 affect code that awaited nothing?Yes. Before SDK 56 `file.copy(dest)` finished before the next line ran. Now it returns a Promise, so an immediate `dest.textSync()` can run before the copy completes. Either `await file.copy(dest)` or switch to `copySync(dest)` for the old semantics.
saying these in an interview costs you the question
- file.write() returns a Promise and must be awaited.
- textSync() runs on a background thread, so it never blocks the UI.
- Constructing a File object creates the file on disk.
- write() creates any missing parent directories automatically.
- copy() and move() are still synchronous in SDK 57.