HotCodePush

API

Every method, event and type of @hotcodepush/react-native-code-push and the useUpdates hook, with a signature and an example each.

The React Native SDK exports one object, HotCodePush, with 16 methods and 5 events, and one hook, useUpdates(). Every method takes at most one options object and resolves a typed result or nothing. Nothing an app should handle is thrown.

src/updates.ts
import { HotCodePush, useUpdates } from '@hotcodepush/react-native-code-push';

The package exports every type on this page, and the examples below assume this import. A result is a discriminated union on status, with a reason where the status needs one. An app switches on typed values and never parses a message. The same object and hook work on Expo through the Expo SDK, which re-exports them.

Methods

MethodWhat it doesResolves
sync()Runs one cycle under the strategies: check, download, apply, report.SyncResult
checkForUpdate()Checks and downloads nothing.CheckForUpdateResult
downloadUpdate()Checks again, downloads and applies per the apply strategies.DownloadUpdateResult
applyUpdate()Applies the downloaded update now.ApplyUpdateResult
notifyReady()Ends the readiness gate under readySignal: manual.NotifyReadyResult
rollbackUpdate()Rolls the running release back now.nothing
clearUpdates()Drops every downloaded update and reloads the embedded bundle.nothing
setRestartAllowed()Holds or releases the restart the SDK performs on its own.nothing
getState()Answers a snapshot of what the SDK knows.GetStateResult
getChannel()Answers the channel in effect and where it came from.GetChannelResult
setChannel()Switches the device to a channel by name or by id.nothing
getDevice()Answers the facts of the device report.GetDeviceResult
setAttributes()Merges the attributes that attribute conditions match.nothing
showDebugScreen()Opens the native debug screen.nothing
addListener()Registers a listener for one event.a handle with remove()
removeAllListeners()Removes every listener.nothing

sync()

sync(options?: SyncOptions): Promise<SyncResult>

One cycle under the strategies, the same the SDK runs on its own at start, on resume and at the interval. It fetches the channel's index, evaluates it on the device, downloads and verifies, applies and reports. Its options override any of the three strategies for this cycle, and a second call while a cycle runs joins the running one.

It resolves once the outcome is known, with one of the statuses:

  • up_to_date, or available when the download stayed manual
  • downloaded with the applyStrategy in force, or applied with the reload following
  • skipped or failed with a reason

A release already downloaded answers downloaded again, never up_to_date. A change made in the console reaches the next sync() within about a minute (How an update reaches a device).

const result = await HotCodePush.sync();
switch (result.status) {
  case 'applied':
    // the reload follows this result
    break;
  case 'downloaded':
    console.log(`release #${result.release.number} applies ${result.applyStrategy}`);
    break;
  case 'available':
    console.log(`release #${result.release.number} waits for downloadUpdate()`);
    break;
  case 'skipped':
  case 'failed':
    console.log(`${result.status}: ${result.reason}`);
    break;
  case 'up_to_date':
    break;
}

checkForUpdate()

checkForUpdate(): Promise<CheckForUpdateResult>

sync() with downloadStrategy pinned to manual: it fetches and evaluates, downloads nothing and moves nothing. It resolves up_to_date, available with the release, its notes and the pack's size, skipped or failed. A release already downloaded answers available too. It counts as the device's check and is recorded as getState().lastCheck.

const result = await HotCodePush.checkForUpdate();
if (result.status === 'available') {
  console.log(`release #${result.release.number}, ${result.downloadSizeBytes} bytes: ${result.notes ?? ''}`);
}

downloadUpdate()

downloadUpdate(options?: DownloadUpdateOptions): Promise<DownloadUpdateResult>

sync() with downloadStrategy pinned to auto: it checks the index again, downloads and verifies, and applies per applyStrategy or mandatoryApplyStrategy, which options overrides for this cycle. It resolves downloaded with the applyStrategy in force, applied, up_to_date, skipped or failed, never available. The cycle is recorded as getState().lastCheck and resets no interval.

const result = await HotCodePush.downloadUpdate({ applyStrategy: 'manual' });
if (result.status === 'downloaded') {
  console.log(`release #${result.release.number} is downloaded; call applyUpdate() when ready`);
}

applyUpdate()

applyUpdate(): Promise<ApplyUpdateResult>

Applies the downloaded update now, whatever the strategy says, and reloads the app, at once or as soon as the app is up. It resolves applied with the release, or nothing_to_apply with the release that runs. A downloaded release that has left the index since is dropped first. Holding restarts with setRestartAllowed() never holds it.

A call still waiting when the run ends is not kept, so the app calls it again. It is how an app on applyStrategy: manual finishes an update, a mandatory one under mandatoryApplyStrategy: manual included. The pattern is in Choose an apply strategy.

const result = await HotCodePush.applyUpdate();
if (result.status === 'applied') {
  console.log(`release #${result.release.number} loads now`);
}

notifyReady()

notifyReady(): Promise<NotifyReadyResult>

Ends the readiness gate when readySignal is manual, and is safe to call at any time on either setting. It also says the app is up in this run, so a reload that waited for that runs. The result says whether this start follows a rollback: isRolledBack with rollbackReason, and previousRelease, each told once. currentRelease is null on the embedded bundle.

const result = await HotCodePush.notifyReady();
if (result.isRolledBack) {
  console.log(`rolled back from #${result.previousRelease?.number}: ${result.rollbackReason}`);
}

rollbackUpdate()

rollbackUpdate(options?: RollbackUpdateOptions): Promise<void>

Rolls the running release back now and reloads at once. It reverts to the last confirmed release or the embedded bundle and marks the bundle as failed on this device. The device reports app_requested with reason as the detail.

It does nothing on the embedded bundle, and reason is at most 256 Unicode code points without a control character. A rollback that reaches every device is Roll back a release.

await HotCodePush.rollbackUpdate({ reason: 'checkout failed to load' });

clearUpdates()

clearUpdates(): Promise<void>

Clears every downloaded update and the list of failed bundles, keeps the device id, channel choice and attributes, and reloads into the embedded bundle. Before the app is up in this run it waits, and setRestartAllowed({ isAllowed: false }) never holds it. It is the escape hatch a support guide can name.

await HotCodePush.clearUpdates();

setRestartAllowed()

setRestartAllowed(options: SetRestartAllowedOptions): Promise<void>

isAllowed: false holds the restart the SDK would perform on its own, at most one, until isAllowed: true runs it. That covers an immediate apply, a next_resume apply, a mandatory release and the reload after a readiness timeout. Until the held restart runs, getState().currentRelease still names what runs. An apply still held when the run ends takes effect at the next start.

A restart the app asks for itself, applyUpdate(), rollbackUpdate() or clearUpdates(), is never held, and every start begins allowed. It is how an app protects a checkout or an unsaved form without giving up automatic applies.

await HotCodePush.setRestartAllowed({ isAllowed: false });
// ... the flow the user must finish ...
await HotCodePush.setRestartAllowed({ isAllowed: true });

getState()

getState(): Promise<GetStateResult>

A snapshot of what the SDK knows, everything the debug screen shows:

  • currentRelease is the running release, null on the embedded bundle, and what an app attaches to its own telemetry.
  • nextRelease is downloaded and waiting for its apply moment.
  • fallbackRelease is the last confirmed release, the one a rollback returns to.
  • embeddedBundleId comes from the resource file and is null in a build whose step created no binary.
  • lastCheck holds the last cycle's time, trigger and result.
  • index is the channel index the device holds, with sequence its publish time in milliseconds, the debug screen's Published at, and fetchedAt when the device fetched it.
  • failedBundleIds lists the bundles that failed on this device and are never tried again here.
  • reportedAt is the server time of the month's first acknowledged report.
const state = await HotCodePush.getState();
console.log(state.currentRelease?.bundleVersion ?? 'embedded', state.lastCheck?.result.status);

getChannel()

getChannel(): Promise<GetChannelResult>

The channel in effect with its source, runtime after setChannel() or config from the build. The id is null while no id is known, on a build without a channel and for a runtime name no sync has resolved yet. The name is the runtime name, null for the build's channel, which the build carries by id alone.

const channel = await HotCodePush.getChannel();
console.log(channel.source, channel.id ?? 'unresolved', channel.name ?? '');

setChannel()

setChannel(options: SetChannelOptions): Promise<void>

Switches the device to a channel, by { name } or by { id }. A name works for a channel flagged discoverable with channel update --discoverable. An id works for a channel the app was handed at runtime: a preview from a QR code, or a tenant's channel from your backend.

Passing null clears the runtime choice and returns to the build's channel. A name is stored and resolved at the next sync, which answers channel_unknown while the channel is not discoverable. A channel flagged a moment ago reaches the device within about a minute.

A deleted or expired channel serves no index: every check fails with channel_unknown, the device keeps the bundle it runs and its channel, and the SDK never changes the channel on its own. A runtime choice is yours to clear with setChannel(null), as Switch channels by link shows.

The patterns around the call are Test with preview channels, Switch channels by link and Set up environments and tenants.

await HotCodePush.setChannel({ name: 'staging' });
const result = await HotCodePush.sync();
if (result.status === 'failed' && result.reason === 'channel_unknown') {
  await HotCodePush.setChannel(null);
}

getDevice()

getDevice(): Promise<GetDeviceResult>

The facts of the device report and nothing more:

  • id is the anonymous per-install id.
  • platform, osVersion and sdkVersion are ios or android, the OS version and the SDK version.
  • binaryVersion and binaryBuild are the store build's version and build number.
  • fingerprint is the native fingerprint the build carries, null in a build that carries none.
  • channel reads as getChannel() answers it.
  • attributes holds every attribute the app set.

What leaves the device is on Privacy and device data.

const device = await HotCodePush.getDevice();
console.log(device.id, device.platform, device.binaryVersion, device.fingerprint);

setAttributes()

setAttributes(options: SetAttributesOptions): Promise<void>

Merges string attributes for attribute conditions, userId, tenant, plan, cohort. A null value removes a key. They are evaluated on the device and reported when they change, which is what Target devices with attributes builds on. A key is letters, digits, _, - and ., at most 64 characters, and a value is at most 256 Unicode code points without a control character.

Attributes are labels, never personal data: an opaque user id yes, an email never.

await HotCodePush.setAttributes({ userId: '42', plan: 'pro', cohort: null });

showDebugScreen()

showDebugScreen(): Promise<void>

Opens the SDK's native debug screen over the app, which shows what the device knows. Check now runs a cycle with the trigger manual, a sync() on iOS and a checkForUpdate() on Android. Share opens the system share sheet with the screen as text. While a runtime choice is in effect, the screen offers Return to configured channel, which calls setChannel(null).

Put the call behind a settings entry or a shake handler. An app may ship it, since nothing on it is secret and the log never leaves the device on its own.

await HotCodePush.showDebugScreen();
SectionLineMeans
DeviceDevice idShows the anonymous id the SDK generated at first start, the one device probe takes.
DeviceBinary, OS, SDKShows the build's version and build number, the OS version and the SDK version.
DeviceFingerprintShows the native fingerprint the build carries, or none.
DeviceDebug build, AttributesShows yes or no, then every key=value the app set, or none.
ChannelChannel id, Name, SourceShows the channel in effect, unresolved for a runtime name no sync has resolved yet, then its name, then config from the build or runtime after setChannel().
ReleasesRunning, Downloaded, FallbackShows the release that runs, the one waiting for its apply moment and the one a rollback returns to.
ReleasesEmbedded bundleShows the id of the bundle the build ships, or not registered when the build step created no binary.
ReleasesFailed bundlesLists the bundles that failed on this device and are never tried again here.
Last checkWhen, Trigger, ResultShows the last cycle's time, what started it, and its status with reason and condition as one code, skipped device_incompatible fingerprint.
Last checkMessageShows the sentence behind the code.
IndexPublished atShows when the channel index the device holds was published.
IndexFetched atShows when the device fetched it, or never.
IndexReported atShows the server time of the month's first acknowledged report, the moment that counts the device as active this month.
ConfigurationBuilt atShows the build time. No release created before it is applied on this binary.
ConfigurationStrategies, Ready signal, Debug builds, Public keysShows the values the build carries, the public keys as a count.
Logevery lineShows this session's log, newest last: the time, a code and a sentence. It holds 200 entries and starts empty at every launch.

addListener()

addListener<EventName extends HotCodePushEventName>(
  eventName: EventName,
  listener: (event: HotCodePushEvents[EventName]) => void,
): Promise<HotCodePushListenerHandle>

Registers a listener for one of the five events and resolves a HotCodePushListenerHandle, { remove: () => Promise<void> }. The updateRolledBack event belongs to the start that follows a rollback, before any listener exists. The native side keeps it, and every listener added during that start receives it once.

const handle = await HotCodePush.addListener('updateDownloaded', event => {
  console.log(`release #${event.release.number} applies ${event.applyStrategy}`);
});
// later, when the screen goes
await handle.remove();

removeAllListeners()

removeAllListeners(): Promise<void>

Removes every listener added through addListener(), the updateRolledBack ones included.

await HotCodePush.removeAllListeners();

Events

No event fires for a cycle's start or end, for a check that found nothing or for a skipped release. The last cycle's outcome is getState().lastCheck. No event fires before a reload.

EventPayloadWhen
updateAvailableUpdateAvailableEventA check found a release the device qualifies for.
updateDownloadedUpdateDownloadedEventThe download completed and the update waits for its apply moment.
updateFailedUpdateFailedEventA check or a download failed.
downloadProgressDownloadProgressEventA pack is downloading.
updateRolledBackUpdateRolledBackEventThe app starts after a rollback.

updateAvailable

{ downloadSizeBytes: number | null; notes: string | null; release: Release; trigger: SyncTrigger }

A check found a release the device qualifies for. downloadSizeBytes is the pack's size, 0 for a release of the embedded bundle, which downloads nothing. The trigger says what started the cycle: start, resume, interval or manual. It is the event a manual download waits on, and the one behind a "new version" badge.

await HotCodePush.addListener('updateAvailable', event => {
  console.log(`release #${event.release.number}, ${event.downloadSizeBytes} bytes, found on ${event.trigger}`);
});

updateDownloaded

{ applyStrategy: 'next_start' | 'next_resume' | 'manual'; release: Release; trigger: SyncTrigger }

The download completed and the update waits for its apply. applyStrategy names the moment, or manual for the app's own applyUpdate(). It never fires for an immediate apply, whose download goes straight to the reload. It is the event behind the restart dialog for a mandatory release under mandatoryApplyStrategy: manual, with release.isMandatory saying so.

await HotCodePush.addListener('updateDownloaded', event => {
  if (event.applyStrategy === 'manual' && event.release.isMandatory) {
    void HotCodePush.applyUpdate();
  }
});

updateFailed

{ message: string; reason: FailedReason; release: Release | null; trigger: SyncTrigger }

A check or a download failed, with the reason from the catalog and a message for the log. A build without a channel starts no automatic check, so there the event follows a call the app made.

await HotCodePush.addListener('updateFailed', event => {
  console.log(`${event.reason}: ${event.message}`);
});

downloadProgress

{ downloadSizeBytes: number; progress: number; progressBytes: number; releaseId: string }

While a pack downloads: progressBytes of downloadSizeBytes have arrived, and progress is their share from 0 to 1.

await HotCodePush.addListener('downloadProgress', event => {
  console.log(`${Math.round(event.progress * 100)} % of ${event.downloadSizeBytes} bytes`);
});

updateRolledBack

{ fromRelease: Release; reason: RollbackReason; toRelease: Release | null }

Fires at the start that follows a rollback, before the readiness gate, and again at each start until the app has come up once. The toRelease is null for the embedded bundle. The event is kept until the first listener of that start registers, so a listener added after the start still receives it.

await HotCodePush.addListener('updateRolledBack', event => {
  console.log(`release #${event.fromRelease.number} rolled back: ${event.reason}`);
});

Hooks

useUpdates()

useUpdates(): UseUpdatesResult

A React hook over the five events and getState(), sugar over the common API and never a capability of its own:

  • state is getState() read on mount and again after each event and each cycle the app runs, and the embedded state until the first read.
  • lastSync is state.lastCheck.result, the SDK's own cycles included, or null before the first.
  • isSyncing is true while a cycle the app started runs or a pack downloads.
  • downloadProgress is the download's progress from 0 to 1 while a pack downloads, null otherwise.
src/ReleaseLabel.tsx
import { HotCodePush, useUpdates } from '@hotcodepush/react-native-code-push';
import { Pressable, Text, View } from 'react-native';

export function ReleaseLabel() {
  const { downloadProgress, isSyncing, lastSync, state } = useUpdates();
  const syncText = isSyncing
    ? downloadProgress === null
      ? 'syncing…'
      : `downloading ${Math.round(downloadProgress * 100)}%`
    : (lastSync?.status ?? 'no check yet');
  return (
    <View>
      <Text>{state.currentRelease?.bundleVersion ?? 'embedded'}</Text>
      <Text>{syncText}</Text>
      <Pressable disabled={isSyncing} onPress={() => void HotCodePush.sync()}>
        <Text>Sync now</Text>
      </Pressable>
    </View>
  );
}

Types

The types as the package exports them, the SDK's own first. The HotCodePush object is typed by the package's HotCodePush interface: the 16 methods above, with addListener resolving HotCodePushListenerHandle.

interface HotCodePushListenerHandle {
  remove: () => Promise<void>;
}

interface UseUpdatesResult {
  // The download's progress from 0 to 1 while a pack downloads, null otherwise.
  downloadProgress: number | null;
  // A cycle the app started is running, or a pack is downloading.
  isSyncing: boolean;
  // The result of the last cycle, the SDK's own included; null before the first.
  lastSync: SyncResult | null;
  state: GetStateResult;
}

The shared types, the same in every SDK:

// The strategies, the values of the matching hotcodepush.json keys.
type CheckStrategy = 'auto' | 'manual';
type DownloadStrategy = 'auto' | 'manual' | 'unmetered';
type ApplyStrategy = 'immediate' | 'manual' | 'next_resume' | 'next_start';
type MandatoryApplyStrategy = 'immediate' | 'manual';
type ReadySignal = 'manual' | 'render';
// When a scheduled apply runs; `manual` is not a moment.
type ApplyMoment = Exclude<ApplyStrategy, 'manual'>;
// What started a cycle: the SDK on its own, or the app's call.
type SyncTrigger = 'interval' | 'manual' | 'resume' | 'start';
type ConditionType = 'attribute' | 'binary' | 'device' | 'fingerprint' | 'os';
type Platform = 'android' | 'ios';

// The SDK's own release type: 5 fields mapped from an index entry.
interface Release {
  bundleId: string;
  bundleVersion: string;
  id: string;
  isMandatory: boolean;
  number: number;
}

// The statuses, as the Statuses table below reads them.
type SyncStatus = 'applied' | 'available' | 'downloaded' | 'failed' | 'skipped' | 'up_to_date';
type ApplyUpdateStatus = 'applied' | 'nothing_to_apply';

type SyncResult =
  | { release: Release | null; status: 'up_to_date' }
  | { downloadSizeBytes: number | null; notes: string | null; release: Release; status: 'available' }
  | { applyStrategy: Exclude<ApplyStrategy, 'immediate'>; notes: string | null; release: Release; status: 'downloaded' }
  | { notes: string | null; release: Release; status: 'applied' }
  | { conditionType?: ConditionType; reason: SkippedReason; release: Release | null; status: 'skipped' }
  | { message: string; reason: FailedReason; release: Release | null; status: 'failed' };

// `sync()` with the download pinned to `manual`: nothing is downloaded or applied.
type CheckForUpdateResult = Exclude<SyncResult, { status: 'applied' | 'downloaded' }>;
// `sync()` with the download pinned to `auto`: the cycle never stops at `available`.
type DownloadUpdateResult = Exclude<SyncResult, { status: 'available' }>;

type ApplyUpdateResult =
  | { release: Release; status: 'applied' }
  | { release: Release | null; status: 'nothing_to_apply' };

interface NotifyReadyResult {
  currentRelease: Release | null;
  isRolledBack: boolean;
  // The release before this start, when it changed.
  previousRelease: Release | null;
  rollbackReason?: RollbackReason;
}

interface GetStateResult {
  currentRelease: Release | null;
  // From the resource file; `null` in a build whose step created no binary.
  embeddedBundleId: string | null;
  failedBundleIds: string[];
  fallbackRelease: Release | null;
  index: { fetchedAt: string; sequence: number } | null;
  lastCheck: { checkedAt: string; result: SyncResult; trigger: SyncTrigger } | null;
  nextRelease: Release | null;
  // The server time of the month's first acknowledged report.
  reportedAt: string | null;
}

type ChannelSource = 'config' | 'runtime';

interface GetChannelResult {
  // `null` while no id is known: a build without a channel, or a runtime name no sync has resolved yet.
  id: string | null;
  name: string | null;
  source: ChannelSource;
}

type SetChannelOptions = { id: string } | { name: string } | null;

interface GetDeviceResult {
  attributes: Record<string, string>;
  binaryBuild: string;
  binaryVersion: string;
  channel: GetChannelResult;
  // `fp1:<sha256>` from the resource file, `null` in a build that carries none.
  fingerprint: string | null;
  id: string;
  osVersion: string;
  platform: Platform;
  sdkVersion: string;
}

// `null` removes a key; stored and reported stays Record<string, string>.
type SetAttributesOptions = Record<string, string | null>;

interface RollbackUpdateOptions {
  // The app-side cause, carried as the failure's detail: at most 256 code points, no control character.
  reason?: string;
}

// Each stage's strategy for this call, overriding the configuration.
interface SyncOptions {
  applyStrategy?: ApplyStrategy;
  downloadStrategy?: DownloadStrategy;
  mandatoryApplyStrategy?: MandatoryApplyStrategy;
}

// The apply strategies for this call; the download strategy is pinned to `auto`.
type DownloadUpdateOptions = Omit<SyncOptions, 'downloadStrategy'>;

interface SetRestartAllowedOptions {
  isAllowed: boolean;
}

type SkippedReason =
  | 'build_debug' | 'bundle_failed_before' | 'channel_paused' | 'condition_unsupported' | 'connection_metered'
  | 'device_incompatible' | 'device_not_in_rollout' | 'device_not_targeted' | 'release_older_than_binary'
  | 'release_revoked' | 'spending_cap_reached';
type FailedReason =
  | 'channel_unknown' | 'content_mismatched' | 'device_offline' | 'download_failed' | 'index_invalid'
  | 'manifest_invalid' | 'signature_invalid';
type RollbackReason = 'app_crashed' | 'app_requested' | 'readiness_timed_out';

// What the device reports: the event rows of Results and reasons, and the pack a download came from.
type DeviceEventType = 'applied' | 'checked' | 'confirmed' | 'downloaded' | 'failed';
type PackKind = 'delta' | 'files' | 'full' | 'streamed';

// The events, by name.
interface UpdateAvailableEvent {
  downloadSizeBytes: number | null;
  notes: string | null;
  release: Release;
  trigger: SyncTrigger;
}
interface UpdateDownloadedEvent {
  applyStrategy: Exclude<ApplyStrategy, 'immediate'>;
  release: Release;
  trigger: SyncTrigger;
}
interface UpdateFailedEvent {
  message: string;
  reason: FailedReason;
  release: Release | null;
  trigger: SyncTrigger;
}
interface DownloadProgressEvent {
  downloadSizeBytes: number;
  progress: number;
  progressBytes: number;
  releaseId: string;
}
interface UpdateRolledBackEvent {
  fromRelease: Release;
  reason: RollbackReason;
  toRelease: Release | null;
}
interface HotCodePushEvents {
  downloadProgress: DownloadProgressEvent;
  updateAvailable: UpdateAvailableEvent;
  updateDownloaded: UpdateDownloadedEvent;
  updateFailed: UpdateFailedEvent;
  updateRolledBack: UpdateRolledBackEvent;
}
type HotCodePushEventName = keyof HotCodePushEvents;

The release is null where none could be evaluated: the index holds nothing for the embedded bundle, could not be read, or the cycle stopped before it. A failure after the evaluation names the release it tried. conditionType on skipped names the condition type behind device_incompatible or device_not_targeted, never a value.

Statuses

Seven statuses, named by what the app should do next.

StatusFromMeaning
up_to_datesync(), checkForUpdate(), downloadUpdate()There is nothing to do. A release that carries the running bundle answers it too.
availablecheckForUpdate(), and sync() when the download stayed manualA release this device can take is not downloaded yet, or was downloaded already when checkForUpdate() asked. Call downloadUpdate().
downloadedsync(), downloadUpdate()The update is downloaded and verified. Under applyStrategy: manual call applyUpdate(), otherwise the strategy's moment applies it.
appliedsync(), downloadUpdate(), applyUpdate()The apply happened, and the update becomes the current release as the app reloads, now or once the app is up in this run.
nothing_to_applyapplyUpdate()Nothing waits.
skippedsync(), checkForUpdate(), downloadUpdate()There is a newer release and this device will not take it now, and reason says why.
failedsync(), checkForUpdate(), downloadUpdate()Something went wrong on the way, and reason says what.

The reason on skipped and failed, and the rollback reasons on updateRolledBack and notifyReady(), are the unions above. Results and reasons is the catalog: what each code means and what to do.

Errors

No error carries a code, and every outcome an app can act on is a result. A method with nothing to do does nothing: applyUpdate() without a downloaded update answers nothing_to_apply, and rollbackUpdate() on the embedded bundle resolves. A method rejects with a plain error, a message and nothing else, for a programming mistake found in development:

  • a strategy value outside its choices on sync() or downloadUpdate()
  • isAllowed that is not a boolean
  • an attribute key or value that breaks its rule, or a value that is neither a string nor null
  • a rollback reason that breaks the value rule
  • a channel id that is not a UUID

A build whose build step never ran has no resource file, and a build whose file the reader refused is the same case with the reason. Every method then rejects with one error until the app is rebuilt. The error says HotCodePush is not configured, names the missing file or the reader's reason, and points at npx hotcodepush init and a rebuild.

Where the native module is missing, on the web or in Expo Go, the package loads and every method rejects with one error, removeAllListeners() included. The error says the SDK needs a development or release build. The useUpdates() hook ignores the rejection and keeps the embedded state.

Guard the calls with the platform check you use for every native-only module, or catch the rejection.

Last updated on

On this page