google/zx · error · Fail
sync mode does not allow async command resolution
Error message
sync mode does not allow async command resolution
What it means
Thrown by ProcessPromise.build() when running in sync mode ($.sync or $({sync:true})) but the resolved command ($.cmd) is not a string. buildCmd returns a non-string when an interpolated value or the quote function yields a Promise; sync mode cannot await, so zx rejects it.
Source
Thrown at src/core.ts:307
}
// prettier-ignore
private build(): void {
const $ = this._snapshot
if (!$.shell)
throw new Fail(`No shell is available: ${Fail.DOCS_URL}/shell`)
if (!$.quote)
throw new Fail(`No quote function is defined: ${Fail.DOCS_URL}/quotes`)
if ($.pieces.some((p) => p == null))
throw new Fail(`Malformed command at ${$.from}`)
$.cmd = buildCmd(
$.quote!,
$.pieces as TemplateStringsArray,
$.args
) as string
if ($[SYNC] && !isString($.cmd))
throw new Fail('sync mode does not allow async command resolution')
}
run(): this {
ProcessPromise.bus.runBack(this)
if (this.isRunning() || this.isSettled()) return this // The _run() can be called from a few places.
this._stage = 'running'
const self = this
const $ = self._snapshot
const { id, cwd } = self
if (!fs.existsSync(cwd)) {
this.finalize(
ProcessOutput.fromError(
new Error(`The working directory '${cwd}' does not exist.`)
)
)
return this
}View on GitHub (pinned to 00a2c484e2)
Solutions
- Resolve async values before the sync call: `const v = await fetch(url).then(r=>r.text()); $.sync\`echo ${v}\``.
- Drop sync mode for async work — use the default async `$` instead of $.sync/{sync:true}.
- Ensure $.quote returns a plain string synchronously.
- Pre-compute all interpolated inputs into strings.
Example fix
// before: getToken() returns a Promise
$.sync`deploy ${getToken()}`
// after: resolve first
const token = await getToken()
$.sync`deploy ${token}` Defensive patterns
Strategy: type-guard
Validate before calling
function assertSyncArgs(args: unknown[]): void {
const pending = args.find((a) => a && typeof (a as any).then === 'function')
if (pending) {
throw new Error('sync mode cannot accept a Promise arg; resolve it first')
}
}
assertSyncArgs(interpolatedArgs) Type guard
const isThenable = (v: unknown): v is Promise<unknown> => !!v && typeof (v as any).then === 'function'
Try / catch
try {
$.sync`cmd ${value}`
} catch (e) {
if (e instanceof Fail && /sync mode does not allow async/.test(e.message)) {
// fall back to async mode
await $`cmd ${value}`
} else throw e
} Prevention
- Pre-resolve every interpolated value to a string before a sync `$` call.
- Reserve $.sync for pure shell strings with no async inputs.
- Ensure any custom $.quote returns a plain string, not a Promise.
When it happens
Trigger: `` $.sync`deploy ${getToken()}` `` where getToken() returns a Promise; `` $({sync:true})`echo ${fetch(url)}` ``; a custom $.quote that returns a Promise; interpolating a ProcessPromise (async) into a sync `$` template.
Common situations: Forgetting a value is async; mixing sync execution with network/file reads; third-party quote functions returning promises; migrating async code to $.sync without resolving inputs.
Related errors
- Malformed command at ${$.from}
- No shell is available: ${Fail.DOCS_URL}/shell
- Too late to abort the process.
AI-assisted analysis of google/zx@00a2c484e2 (2026-08-13).
Data as JSON: /api/errors/19fa38ff7f5398d2.
Report an issue: GitHub.