skip to content

In React Native Async Storage v3, how do getMany, setMany and removeMany behave, and how should you handle the AsyncStorageError they can throw?

level: middleimportance: should knowfreq 34%

answer

  1. what you request is what you get
  2. missing keys come back null
  3. batches are all-or-nothing
  4. one error class, check e.type
  5. NativeModuleError means module not found

basics

~20 s

getMany returns an object containing every requested key, null for missing ones; setMany takes a key-to-string record; removeMany ignores absent keys. Each batch is atomic, and any failure throws AsyncStorageError, whose type property says which layer failed.

solid answer

~40 s

`getMany(keys)` resolves to a `Record<string, string | null>` that contains **every key I asked for**, with `null` where nothing is stored, so I never have to check whether a key is present. `setMany({ ... })` takes a record of string values, and `removeMany(keys)` silently skips keys that are not there. The library documents all three as **atomic**: if part of a batch fails, the whole batch is rolled back and nothing is partially written. Every failure is an `AsyncStorageError`; I catch it, check `instanceof`, and switch on `e.type` against `AsyncStorageError.Type`: `NativeModuleError` (the native module is missing), `SqliteStorageError`, `WebStorageError` (IndexedDB), `OtherStorageError` or `UnknownError`. For startup preferences I fall back to defaults on a read error rather than crashing.

go deeper

for a junior

Recall the three batch methods, that getMany returns every requested key with null for missing ones, and that failures throw AsyncStorageError.

for a middle

Explain why a batch beats a loop of single calls (one native call, one transaction) and list what each AsyncStorageError type points to.

for a senior

Design error handling per call site: degrade on preference reads, surface failed user-initiated writes, and log the error type so build problems are fixed once.

for a principal

Decide which writes must be grouped atomically and where the app must tolerate being killed between batches, since atomicity stops at one call.

## The batch methods Version 3 of `@react-native-async-storage/async-storage` has three batch methods on every storage instance: | Method | Input | Result | Missing keys | |---|---|---|---| | `getMany(keys)` | `string[]` | `Promise<Record<string, string \| null>>` | present in the result with `null` | | `setMany(entries)` | `Record<string, string>` | `Promise<void>` | created or overwritten | | `removeMany(keys)` | `string[]` | `Promise<void>` | ignored, no error | The migration guide calls the `getMany` contract **"what you request is what you get"**: every key in the request appears in the returned object. That differs from v2's `multiGet`, which returned an array of `[key, value]` pairs you had to search. ```typescript const prefs = createAsyncStorage("preferences"); const values = await prefs.getMany(["onboardingDone", "language"]); // { onboardingDone: "true", language: null } when no language was saved ``` ## Why batch instead of looping Reading two startup preferences with two `getItem` calls works, but a batch is better for three reasons: 1. **One call across the native boundary.** Each method is a Turbo Module call into native code; the batch sends all keys at once and the native side runs one query for the list. 2. **Atomicity.** The library documents batch operations as all-or-nothing. On native platforms the writes run in one SQLite transaction; if any part fails, the batch is rolled back and an `AsyncStorageError` is thrown. Two separate `setItem` calls can leave the first written and the second not. 3. **A simpler shape.** A record keyed by the requested keys needs no lookup code. Atomicity is per call. Two `setMany` calls issued one after the other are two transactions, and the app can be killed between them. ## AsyncStorageError Every method on a v3 instance throws one class, `AsyncStorageError`, which extends `Error` and adds a `type` field. The values live on `AsyncStorageError.Type`: - **`NativeModuleError`**: the React Native native module is null or not initialised at startup. A typical cause is a binary built before the package was installed, so the JavaScript is present but the native side is not. - **`SqliteStorageError`**: SQLite itself failed on iOS, macOS or Android (for example the disk is full or the file is unreadable). - **`WebStorageError`**: an IndexedDB operation failed in a web build. - **`OtherStorageError`**: storage-level failures outside those groups, such as storage that could not be initialised or malformed output from native code. - **`UnknownError`**: anything that could not be classified. The `name` field is derived from the class and is the same for every `AsyncStorageError`, so branch on `type`, not on `name` or on message text. ## Handling errors sensibly ```typescript import { AsyncStorageError, createAsyncStorage } from "@react-native-async-storage/async-storage"; const prefs = createAsyncStorage("preferences"); export async function loadPrefs() { try { return await prefs.getMany(["onboardingDone", "language"]); } catch (e) { if (e instanceof AsyncStorageError && e.type === AsyncStorageError.Type.NativeModuleError) { console.error("Async Storage native module missing; rebuild the app"); } return { onboardingDone: null, language: null }; } } ``` A few guidelines: - **Reads of preferences should degrade**, not crash: fall back to defaults and let the user carry on. - **Writes deserve a visible outcome** when the user just made a choice; if saving the language fails, say so rather than pretending it stuck. - **Report the `type`** to your logs. A `NativeModuleError` is a build problem to fix once, not a runtime condition to retry. - Do not retry in a tight loop on `SqliteStorageError`; a full disk will not clear itself in a few milliseconds. ## Choosing between single and batch calls A quick rule of thumb for each call site: 1. **One key, one moment**: `getItem`/`setItem` read most clearly, for example saving the language when the user taps it. 2. **Several keys needed together**: use `getMany` at startup so defaults are applied in one place. 3. **Several values that must change together**: use `setMany`, because only a single call is atomic; a flag and the value it describes belong in one call. 4. **Cleaning up a feature's keys**: `removeMany` with an explicit list, or `clear()` if the feature has an instance to itself. ## What batches do not give you Batches do not merge: v3 removed `mergeItem` and `multiMerge`, so updating one field of a stored JSON object still means reading, changing and rewriting the whole string. And there is no query: `getAllKeys()` returns every key in the instance, and anything smarter is filtering in JavaScript.

  • You call setMany with the language and the onboarding flag, and the write fails. What is on disk afterwards?
    Neither new value. The library documents batch operations as atomic: a failure rolls back the whole batch and throws `AsyncStorageError`, so both keys keep whatever they held before. That guarantee covers a single call only, not two consecutive batches.
  • Every Async Storage call throws a NativeModuleError right after you installed the package. What is the likely cause?
    The JavaScript package is present but the native module is not registered in the running binary, typically because the app was not rebuilt after installation (a new pod install and native build, or a new development build in Expo). Retrying at runtime will not help; rebuilding does.

saying these in an interview costs you the question

  • Expecting getMany to omit keys that are not stored
  • Assuming a failed setMany leaves the earlier entries written
  • Branching on error.name or message text instead of error.type
  • Treating removeMany on a missing key as an error to catch
  • Retrying a NativeModuleError at runtime instead of rebuilding the app