The File System

expo-file-system File and Directory objects, the document and cache directories, downloads, and what the OS may delete.

Syntaxconst file = new File(Paths.document, 'name.txt'); file.write('text');

Your app can read and write inside its own sandbox, and nowhere else. expo-file-system models that sandbox with two classes, File and Directory, plus a Paths object holding the roots. Install it with npx expo install expo-file-system.

Objects, not paths

new File(Paths.document, 'reports', 'may.pdf') builds an object from path segments. It touches nothing: the file need not exist. exists, size and uri describe it; create(options?), write(contents), text() or textSync(), bytes() and delete() act on it. Directory mirrors that with create(), list() and delete(). This is not the browser's File object from a file input — it is a handle on a path on this device.

Two directories, two lifetimes

Paths.document is for files that must survive: the system will not remove them. Paths.cache is for files you can regenerate, and the system may delete them when storage runs low, without telling you. Check exists before reading from the cache, every time. Paths.bundle holds assets shipped with the app and is read-only.

Downloads

await File.downloadFileAsync(url, destination) takes a File or a Directory and resolves with the File it wrote.

The legacy API

The older function-style API (readAsStringAsync and friends) throws at runtime in SDK 57 unless imported from expo-file-system/legacy.

Example

Example · tsx
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { Directory, File, Paths } from 'expo-file-system';

const REPORT_URL = 'https://example.com/report.pdf';

export default function ReportDownload() {
  const [status, setStatus] = useState('Not downloaded');

  async function download() {
    setStatus('Downloading');
    // Documents survive; the system will not delete this folder on its own.
    const folder = new Directory(Paths.document, 'reports');
    // Constructing the object did nothing on disk. create() does.
    if (!folder.exists) {
      folder.create({ intermediates: true });
    }
    const file = await File.downloadFileAsync(REPORT_URL, folder);
    setStatus(`Saved ${file.size} bytes`);
  }

  return (
    <View style={styles.screen}>
      <Text style={styles.status}>{status}</Text>
      <Pressable style={styles.button} onPress={download}>
        <Text style={styles.buttonText}>Download report</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 },
  status: { fontSize: 16 },
  button: { backgroundColor: '#1f6feb', paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 },
  buttonText: { color: '#ffffff', fontSize: 16 },
});

When to use it

  • Cache downloaded images or audio under Paths.cache so a second view costs nothing.
  • Save a generated PDF or CSV to the document directory before handing it to the share sheet.
  • Keep a copy of a photo picked by the user while its upload is still queued.
  • Write a rolling debug log a tester can send you from their device.

More examples

Writing and reading a file

Every read has an async and a sync form, and they return the same thing. Use the async one anywhere near a render; the sync one is for small files at startup, where awaiting would mean an extra frame of blank screen. write() replaces the whole contents by default, but it takes a second options argument: write(line, { append: true }) adds to the end of the file instead of rewriting it, which is what a rolling log wants.

Example · tsx
import { File, Paths } from 'expo-file-system';

export async function roundTrip() {
  const draft = new File(Paths.document, 'draft.json');
  // Without overwrite, create() on an existing file throws.
  draft.create({ intermediates: true, overwrite: true });
  draft.write(JSON.stringify({ body: 'hello' }));

  const viaPromise = await draft.text(); // off the JS thread
  const viaSync = draft.textSync();      // same bytes, blocks the JS thread

  console.log(viaPromise === viaSync, draft.exists);
  // Output: true true
}

Pruning the cache directory

Reach for this when caching images or audio yourself: the cache directory grows until the OS intervenes, and the OS intervenes bluntly. The instanceof narrowing is required, not decoration — list() hands back Directory objects alongside the files, and a Directory's size is number | null while a File's is a plain number, so without the narrowing the union is possibly null and TypeScript rejects the comparison, and you would be deleting whole folders as well as files. The early return is the habit to copy: anything under Paths.cache may be gone by the next launch.

Example · tsx
import { Directory, File, Paths } from 'expo-file-system';

export function clearLargeThumbnails() {
  const folder = new Directory(Paths.cache, 'thumbnails');
  if (!folder.exists) {
    // The system may have removed it since last launch. That is normal.
    return;
  }
  for (const entry of folder.list()) {
    // list() returns File and Directory objects mixed together.
    if (entry instanceof File && entry.size > 1_000_000) {
      entry.delete();
    }
  }
}

Reaching the legacy API

Most tutorials and a good many libraries still use readAsStringAsync, documentDirectory and writeAsStringAsync. They still work, from the /legacy entry point, and the two styles interoperate through file.uri. Use this to keep moving when a third-party package hands you a uri string, not as the style for new code: the legacy API is deprecated, and the File and Directory classes are what the next SDK builds on.

Example · tsx
// SDK 57: these function names throw if imported from 'expo-file-system'.
import * as FileSystem from 'expo-file-system/legacy';
import { File, Paths } from 'expo-file-system';

export async function readWithLegacyApi() {
  const file = new File(Paths.document, 'draft.json');
  const contents = await FileSystem.readAsStringAsync(file.uri);
  return contents;
}

Discussion

  • Be the first to comment on this lesson.