mastra-ai/mastra · error

MastraFactory.prepare() called twice

Error message

MastraFactory.prepare() called twice

What it means

prepare() performs one-time initialization: it seeds the runtime registry and runs one-time adapter init, producing the MastraArgs for the entry file's new Mastra(...) call. A synchronous #preparing guard is set before the first await so overlapping calls — not just sequential ones — cannot double-seed state. Calling prepare() a second time, while a call is in flight or after completion, throws this error.

Source

Thrown at mastracode/factory/src/factory.ts:310

      throw new Error(
        "MastraFactory: 'storage' is required. Pass a FactoryStorage backend — e.g. " +
          "new PgFactoryStorage({ connectionString }) from '@mastra/pg' for deployments, or " +
          "new LibSQLFactoryStorage({ url }) from '@mastra/libsql' for local dev.",
      );
    }
    this.#config = config;
  }

  /**
   * Resolve feature readiness, wire every dependency explicitly, and assemble
   * everything needed to construct the server-owned Mastra. Returns the args
   * for the `new Mastra(...)` literal that must live in the entry file.
   */
  async prepare(): Promise<MastraArgs> {
    // Guard set synchronously (before the first await) so overlapping calls —
    // not just strictly sequential ones — can't double-seed the runtime
    // registry or double-run one-time adapter init.
    if (this.#preparing) throw new Error('MastraFactory.prepare() called twice');
    this.#preparing = true;

    const publicOrigin = (this.#config.publicUrl ?? 'http://localhost:4111').replace(/\/+$/, '');
    const allowedOrigins = (this.#config.allowedOrigins ?? []).map(o => o.replace(/\/+$/, '')).filter(Boolean);
    const storage = this.#config.storage;
    const vector = this.#config.vector;
    const pubsub = this.#config.pubsub;
    // Default auth: honor an explicitly-passed provider (including `null` to
    // disable auth) as-is; otherwise fall back to `MastraAuthStudio`
    // (platform-proxied identity). The default derives its cookie domain
    // from `publicUrl` — deploys on `<sub>.mastra.cloud` mint parent-domain
    // cookies without the caller wiring `MASTRA_COOKIE_DOMAIN` explicitly.
    const configuredAuth = this.#config.auth;
    const auth: IMastraAuthProvider | undefined =
      configuredAuth === null ? undefined : (configuredAuth ?? buildDefaultStudioAuth(publicOrigin));
    if (auth && !this.#config.secretEncryption) {
      console.warn(
        "[factory] auth is enabled but 'secretEncryption' is not configured. Persisted model credentials, " +

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Call prepare() exactly once per process and reuse the returned MastraArgs; cache the promise if multiple callers need it (const argsPromise = factory.prepare()).
  2. Refactor so only a single startup entry point invokes prepare(); other code should receive the cached MastraArgs.
  3. Guard call sites with a module-level singleton: export const prepared = prepareOnce() where prepareOnce memoizes the promise.
  4. Restart the process instead of re-calling prepare() after a failure; the #preparing flag is not reset.
  5. Check duplicated wrappers such as prepareFactory or init helpers that each call prepare() internally.

Example fix

// before
const args1 = await factory.prepare();
const args2 = await factory.prepare(); // throws

// after
const args = await factory.prepare(); // once, shared
export const mastra = new Mastra(args);
Defensive patterns

Strategy: fallback

Validate before calling

// memoize the prepare call so it can never run twice
let preparePromise = null;
export function prepareOnce(factory) {
  preparePromise ??= factory.prepare();
  return preparePromise;
}

Type guard

function isPrepared(factory) {
  return factory != null && typeof factory.prepare === 'function';
}
// then guard usage: only call prepare() when no cached MastraArgs exists

Try / catch

let args;
try {
  args = await prepareOnce(factory);
} catch (err) {
  if (err.message.includes('prepare() called twice')) {
    // a concurrent call already ran prepare; await the shared promise
    args = await preparePromise;
  } else throw err;
}

Prevention

When it happens

Trigger: Calling factory.prepare() more than once: e.g. invoking it both in an entry file and in a helper like prepareFactory, calling it again after an await, or racing two concurrent prepare() calls (the guard is set synchronously before the first await, so overlapping calls are also rejected).

Common situations: Dev-server hot reload re-executing an init module; accidentally awaiting prepare() in two startup paths (e.g. both a top-level init and a route handler); wrapping prepare() in retry logic that re-invokes it after a partial failure.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/b7cf8b7771b0213a. Report an issue: GitHub.