tursodatabase/turso · error · Error

remoteWritesExperimental requires a non-null URL

Error message

remoteWritesExperimental requires a non-null URL

What it means

Thrown from the Database constructor when remoteWritesExperimental is enabled and the url option is a lazy provider function (url?: string | (() => string | null)) whose call returns null. The RemoteWriter that forwards writes to the remote server needs a concrete URL at construction time, so resolveUrl() invokes the provider immediately and throws when it yields null. Note the asymmetry: the plain sync engine tolerates a null lazy URL (it just sets bootstrapIfEmpty: false), but remoteWritesExperimental does not.

Source

Thrown at bindings/javascript/sync/packages/wasm/promise-turbopack-hack.ts:45

        async write(path: string, data: Buffer | Uint8Array): Promise<void> {
            values.set(path, data);
        }
    }
};

async function init(): Promise<Worker> {
    await initThreadPool();
    if (MainWorker == null) {
        throw new Error("panic: MainWorker is not initialized");
    }
    return MainWorker;
}

function resolveUrl(url: string | (() => string | null)): string {
    if (typeof url === "function") {
        const resolved = url();
        if (resolved == null) {
            throw new Error("remoteWritesExperimental requires a non-null URL");
        }
        return resolved;
    }
    return url;
}

class Database extends DatabasePromise {
    #runner: Runner;
    #engine: any;
    #io: ProtocolIo;
    #guards: SyncEngineGuards;
    #worker: Worker | null;
    #remoteWriter: RemoteWriter | null = null;
    #db: any;
    constructor(opts: DatabaseOpts) {
        if (opts.url == null) {
            const db = new NativeDatabase(opts.path, { tracing: opts.tracing, experimental: opts.experimental }) as any;
            super(

View on GitHub (pinned to bad083fafb)

Solutions

  1. Make sure the URL source is populated before construction: set TURSO_DATABASE_URL in the environment (or .env.local) for the failing environment and confirm it is present at runtime, not only on your machine.
  2. Pass a concrete string URL when using remoteWritesExperimental: url: process.env.TURSO_DATABASE_URL! (assert non-null yourself before constructing).
  3. Enable remote writes conditionally: remoteWritesExperimental: url != null, so construction degrades to local/sync-only mode instead of crashing when the URL is absent.
  4. If you intended offline-first lazy sync without remote writes, remove remoteWritesExperimental; the sync engine alone accepts a null lazy URL.

Example fix

// before — throws in the constructor when the env var is missing at that moment
const db = new Database({
    path: "app.db",
    remoteWritesExperimental: true,
    url: () => process.env.TURSO_DATABASE_URL ?? null,
});

// after — resolve the URL first, enable remote writes only when it exists
const url = process.env.TURSO_DATABASE_URL ?? null;
const db = new Database({
    path: "app.db",
    url: url ?? undefined,
    remoteWritesExperimental: url != null,
});
Defensive patterns

Strategy: validation

Validate before calling

import { Database, type DatabaseOpts } from "@tursodatabase/sync-wasm";

const lazyUrl = () => process.env.TURSO_DATABASE_URL ?? null;

// Resolve the URL BEFORE constructing so the constructor can never throw.
const url = lazyUrl();
const opts: DatabaseOpts = {
    path: "app.db",
    url: url ?? undefined,
    remoteWritesExperimental: url != null, // commit to remote writes only with a real URL
};
const db = new Database(opts);

Type guard

type LazyUrl = string | (() => string | null);

function resolvesToUrl(url: LazyUrl | undefined): url is string {
    if (typeof url === "string") return url.length > 0;
    if (typeof url === "function") return url() != null;
    return false;
}

// use: if (resolvesToUrl(opts.url)) { /* safe to set remoteWritesExperimental */ }

Try / catch

try {
    db = new Database({ path, remoteWritesExperimental: true, url: lazyUrl });
} catch (e) {
    if (e instanceof Error && e.message.includes("requires a non-null URL")) {
        // Configuration problem, not a transient failure — fail loudly with context.
        throw new Error("TURSO_DATABASE_URL must be set before enabling remoteWritesExperimental");
    }
    throw e; // never swallow unrelated constructor errors
}

Prevention

When it happens

Trigger: new Database({ path, remoteWritesExperimental: true, url: () => process.env.TURSO_DATABASE_URL ?? null }) where the env var (or any config store the closure reads) is unset at the moment the Database is constructed. Any lazy provider returning null (KV read, platform binding attached late, not-yet-loaded config) hits the same path. The same code ships as promise-bundle.ts, promise-default.ts and promise-turbopack-hack.ts, so the stack trace names whichever bundler entry point your build selected.

Common situations: Next.js/Vite/Turbopack builds where process.env values are inlined at build time and the variable was not configured for that environment; serverless/edge runtimes where the binding is attached after module evaluation; CI or preview deployments missing the TURSO_DATABASE_URL secret; copy-pasting the lazy-url pattern from plain-sync examples while also enabling remoteWritesExperimental.

Related errors


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/c9c293c8444621ad. Report an issue: GitHub.