tursodatabase/turso · error · napi::Error

GenericFailure

GenericFailure

Error message

sync engine operation failed: {err}

What it means

The JS sync binding drives DatabaseSyncEngine as a coroutine. When the generator completes with Err, resume converts the turso_sync_engine error to a napi GenericFailure: "sync engine operation failed: {err}". This is the terminal failure of one sync session, not a single retryable IO step - the completed coroutine cannot be resumed.

Source

Thrown at bindings/javascript/sync/src/generator.rs:29

    fn resume(&mut self, result: Option<String>) -> napi::Result<GeneratorResponse>;
}

impl<F: Future<Output = turso_sync_engine::Result<()>>> Generator
    for genawaiter::sync::Gen<SyncEngineIoResult, turso_sync_engine::Result<()>, F>
{
    fn resume(&mut self, error: Option<String>) -> napi::Result<GeneratorResponse> {
        let result = match error {
            Some(err) => Err(turso_sync_engine::errors::Error::DatabaseSyncEngineError(
                format!("JsSyncEngineIo error: {err}"),
            )),
            None => Ok(()),
        };
        match self.resume_with(result) {
            genawaiter::GeneratorState::Yielded(SyncEngineIoResult::IO) => {
                Ok(GeneratorResponse::IO)
            }
            genawaiter::GeneratorState::Complete(Ok(())) => Ok(GeneratorResponse::Done),
            genawaiter::GeneratorState::Complete(Err(err)) => Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("sync engine operation failed: {err}"),
            )),
        }
    }
}

#[napi]
pub struct SyncEngineChanges {
    pub(crate) status: Box<Option<DbChangesStatus>>,
}

#[napi(discriminant = "type", object_from_js = false)]
pub enum GeneratorResponse {
    IO,
    Done,
    SyncEngineStats {
        cdc_operations: i64,

View on GitHub (pinned to 244cde92a7)

Solutions

  1. Read the wrapped engine error after the prefix - it distinguishes auth, protocol, and engine failures
  2. Refresh the auth token and verify the sync endpoint URL and schema
  3. If the local database diverged, re-bootstrap from the server into a fresh database
  4. Construct a new SyncEngine afterwards - this coroutine is finished

Example fix

// before
await engine.ioLoopAsync(); // rejects with 'sync engine operation failed: ...'
// after - classify and restart
try {
  await engine.ioLoopAsync();
} catch (e) {
  if (String(e.message).startsWith('sync engine operation failed:')) {
    engine.close();
    engine = new SyncEngine(opts); // fresh session
  }
}
Defensive patterns

Strategy: retry

Validate before calling

const res = await fetch(syncUrl, { method: 'HEAD' }).catch(() => null);
if (!res) throw new Error('sync endpoint unreachable before sync start');

Type guard

function isSyncEngineFailure(e: unknown): boolean {
  return e instanceof Error && e.message.startsWith('sync engine operation failed:');
}

Try / catch

try { await runSync(engine); } catch (e) { if (isSyncEngineFailure(e)) { engine.close(); engine = new SyncEngine(opts); } else throw e; }

Prevention

When it happens

Trigger: The sync coroutine ends in an error state: authentication rejected by the sync server, an unrecoverable protocol or session error, local database state incompatible with the server, or a JS-side IO error fed back in via resume(Some(err)).

Common situations: Expired or wrong auth token, sync URL pointing at a non-sync endpoint, server schema changed after an upgrade, or the local replica diverged (e.g. restored from an old backup).

Related errors


AI-assisted analysis of tursodatabase/turso@244cde92a7 (2026-08-20). Data as JSON: /api/errors/921deae5e52a3950. Report an issue: GitHub.