immich-app/immich · info · PigeonError

NO_TOKEN

NO_TOKEN

Error message

No stored change token

What it means

getMediaChanges in MessagesImpl.swift throws PigeonError(code: "NO_TOKEN") when no previously persisted PhotoKit change token exists. PhotoKit change tokens let the app fetch only incremental media changes since the last sync; without a stored token the delta cannot be computed, so a full sync is required instead. The error is a signal to the Dart side to fall back to a full media sync and then persist the new token.

Solutions

  1. Catch the NO_TOKEN code on the Dart side and perform a full media sync (getMediaItems) instead of an incremental one.
  2. After the full sync succeeds, persist the current PHPhotoLibrary change token via the platform channel so future calls return deltas.
  3. Verify the code that stores the token after a successful sync actually runs (e.g. it is not skipped on background termination).

Example fix

// before
try await syncApi.getMediaChanges();
// after
try {
  delta = await syncApi.getMediaChanges();
} on PlatformException catch (e) {
  if (e.code == 'NO_TOKEN') {
    delta = await fullSyncAndStoreToken(); // full fetch, then save token
  } else {
    rethrow;
  }
}
Defensive patterns

Strategy: fallback

Validate before calling

// Dart: check whether a token has been stored before requesting deltas
final hasToken = await prefs.getBool('hasChangeToken') ?? false;

Try / catch

try {
  delta = await api.getMediaChanges();
} on PlatformException catch (e) {
  if (e.code == 'NO_TOKEN') delta = await fullSync(); else rethrow;
}

Prevention

When it happens

Trigger: Calling getMediaChanges before any full sync has ever stored a change token (e.g. fresh install, app data cleared, or getMediaChanges called before the initial full media listing).

Common situations: First launch of the app; after reinstall or 'clear data'; after a bug or migration wiped the stored token from UserDefaults/keychain; calling the incremental sync endpoint from a fresh client that never completed an initial sync.

Understand the failure class

Background: "missing required config value" errors: why libraries refuse to start when a configuration key is empty, unset, or blank — this error's family across 48 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/0ca24d4c30e42d3d. Report an issue: GitHub.

Appendix: source

Thrown at mobile/ios/Runner/Sync/MessagesImpl.swift:157

  }
  
  func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
    runSync(completion) { try $0.getMediaChanges() }
  }
  
  private func getMediaChanges() throws -> SyncDelta {
    guard #available(iOS 16, *) else {
      throw PigeonError(code: kUnsupportedOSError, message: "This feature requires iOS 16 or later.", details: nil)
    }
    
    guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
      throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
    }
    
    guard let storedToken = getChangeToken() else {
      // No token exists, definitely need a full sync
      print("MediaManager::getMediaChanges: No token found")
      throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
    }
    
    let currentToken = PHPhotoLibrary.shared().currentChangeToken
    if storedToken == currentToken {
      return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
    }
    
    let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
    
    var updatedAssets: Set<AssetWrapper> = []
    var deletedAssets: Set<String> = []
    
    for change in changes {
      try Task.checkCancellation()
      guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
      
      let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
      deletedAssets.formUnion(details.deletedLocalIdentifiers)

View on GitHub (pinned to e55ac299a4)