skip to content

In expo-file-system's File class, how do you write and read file contents, and which of those calls block the JavaScript thread?

level: middleimportance: must knowfreq 42%

answer

  1. write is synchronous, returns void
  2. text, base64, bytes return Promises
  3. every read has a *Sync twin
  4. copy and move async since SDK 56
  5. sync native calls run on the JS thread

basics

~20 s

expo-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 s

With `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 lines
typescript
import { 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

for a junior

Recall that you build a File from Paths plus a name, write with write() and read with text() or textSync().

for a middle

Explain which calls are synchronous, that synchronous native functions run on the JavaScript thread, and that copy and move now return Promises.

for a senior

Show when sync I/O becomes a jank source, and move large reads to async calls, streams or a FileHandle with chunked reads.

for a principal

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.