+1 (415) 943-4271

Migrating expo-file-system to the SDK 54+ File API

Expo SDK 54 (2025) replaced expo-file-system's function-style API with an object-oriented File/Directory API. The old functions still exist — but only under expo-file-system/legacy, and new projects resolve the new API by default. If your app was written before SDK 54, plain upgrades will break every FileSystem.* call site.

This guide is the migration map we use in client work.

The escape hatch (use it first)

The fastest way to get an upgraded app compiling is one import swap:

// before
import * as FileSystem from "expo-file-system";
// after — behaviour identical to pre-SDK-54
import * as FileSystem from "expo-file-system/legacy";

Do this everywhere, ship your upgrade, then migrate file by file. Do not mix the two styles in one module.

API mapping

LegacyNew API
FileSystem.documentDirectoryPaths.document
FileSystem.cacheDirectoryPaths.cache
getInfoAsync(uri).existsfile.exists / directory.exists (synchronous property)
makeDirectoryAsync(uri)new Directory(...).create()
readAsStringAsync(uri)file.text() (also .bytes(), .base64())
writeAsStringAsync(uri, s)file.write(s)
deleteAsync(uri)file.delete() / directory.delete()
copyAsync({ from, to })file.copy(destination)
moveAsync({ from, to })file.move(destination)
downloadAsync(url, uri)File.downloadFileAsync(url, file)
readDirectoryAsync(uri)directory.list()

Two mental-model shifts do most of the work:

  1. Paths are objects, not strings. You compose File and Directory instances from Paths.document / Paths.cache rather than concatenating URI strings. Interop with APIs that want a string URI goes through file.uri.
  2. Cheap operations are synchronous. exists, size, list() are plain property/method calls — no more await getInfoAsync just to check existence. I/O that genuinely blocks (downloads, large reads) stays async.

Gotchas from real migrations

  • Don't cache exists. It is a live property; capture-and-reuse patterns from the getInfoAsync era read stale.
  • Error handling changes shape. Legacy calls resolved with { exists: false }; the new API throws typed errors for missing files on read. Wrap reads that used to "fail soft".
  • Upload/download tasks. Resumable transfers and progress move to dedicated task objects — if you used createDownloadResumable, budget time for that call site; it is the one place the migration is a rewrite, not a rename.
  • Third-party libraries that accept fileUri strings keep working — pass file.uri.

Stuck on a big file-heavy codebase? This migration is squarely inside our Expo consulting practice.