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

  1. Resolve async values before the sync call: `const v = await fetch(url).then(r=>r.text()); $.sync\`echo ${v}\``.
  2. Drop sync mode for async work — use the default async `$` instead of $.sync/{sync:true}.
  3. Ensure $.quote returns a plain string synchronously.
  4. 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

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


AI-assisted analysis of google/zx@00a2c484e2 (2026-08-13). Data as JSON: /api/errors/19fa38ff7f5398d2. Report an issue: GitHub.