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
| Legacy | New API |
|---|---|
FileSystem.documentDirectory | Paths.document |
FileSystem.cacheDirectory | Paths.cache |
getInfoAsync(uri).exists | file.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:
- Paths are objects, not strings. You compose
FileandDirectoryinstances fromPaths.document/Paths.cacherather than concatenating URI strings. Interop with APIs that want a string URI goes throughfile.uri. - Cheap operations are synchronous.
exists,size,list()are plain property/method calls — no moreawait getInfoAsyncjust 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 thegetInfoAsyncera 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
fileUristrings keep working — passfile.uri.
Stuck on a big file-heavy codebase? This migration is squarely inside our Expo consulting practice.