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
- Call prepare() exactly once per process and reuse the returned MastraArgs; cache the promise if multiple callers need it (const argsPromise = factory.prepare()).
- Refactor so only a single startup entry point invokes prepare(); other code should receive the cached MastraArgs.
- Guard call sites with a module-level singleton: export const prepared = prepareOnce() where prepareOnce memoizes the promise.
- Restart the process instead of re-calling prepare() after a failure; the #preparing flag is not reset.
- 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
- Expose a single module-level prepareOnce() and never call factory.prepare() directly elsewhere
- Cache the returned MastraArgs and pass them to every consumer
- Never put prepare() inside retry loops or hot-reload re-executed code
- Have secondary entry points (tests, scripts) import the shared prepared instance
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
- MastraAuthBetterAuth is not initialized — init() must run fi
- Shared browser not launched. Call createSharedSession() firs
- Browser not launched
- App deletion failed: ${data.error}
- SlackProvider not attached to Mastra. Call __attach() first.
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/b7cf8b7771b0213a.
Report an issue: GitHub.