{"record":{"id":"19fa38ff7f5398d2","repo":"google/zx","slug":"sync-mode-does-not-allow-async-command-resolution","errorCode":null,"errorMessage":"sync mode does not allow async command resolution","messagePattern":"sync mode does not allow async command resolution","errorType":"exception","errorClass":"Fail","httpStatus":null,"severity":"error","filePath":"src/core.ts","lineNumber":307,"sourceCode":"  }\n  // prettier-ignore\n  private build(): void {\n    const $ = this._snapshot\n    if (!$.shell)\n      throw new Fail(`No shell is available: ${Fail.DOCS_URL}/shell`)\n    if (!$.quote)\n      throw new Fail(`No quote function is defined: ${Fail.DOCS_URL}/quotes`)\n    if ($.pieces.some((p) => p == null))\n      throw new Fail(`Malformed command at ${$.from}`)\n\n    $.cmd = buildCmd(\n      $.quote!,\n      $.pieces as TemplateStringsArray,\n      $.args\n    ) as string\n\n    if ($[SYNC] && !isString($.cmd))\n      throw new Fail('sync mode does not allow async command resolution')\n  }\n  run(): this {\n    ProcessPromise.bus.runBack(this)\n    if (this.isRunning() || this.isSettled()) return this // The _run() can be called from a few places.\n    this._stage = 'running'\n\n    const self = this\n    const $ = self._snapshot\n    const { id, cwd } = self\n\n    if (!fs.existsSync(cwd)) {\n      this.finalize(\n        ProcessOutput.fromError(\n          new Error(`The working directory '${cwd}' does not exist.`)\n        )\n      )\n      return this\n    }","sourceCodeStart":289,"sourceCodeEnd":325,"githubUrl":"https://github.com/google/zx/blob/00a2c484e219c2e84bfc3a199febf7fbce2cfbf4/src/core.ts#L289-L325","documentation":"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.","triggerScenarios":"`` $.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.","commonSituations":"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.","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."],"exampleFix":"// before: getToken() returns a Promise\n$.sync`deploy ${getToken()}`\n// after: resolve first\nconst token = await getToken()\n$.sync`deploy ${token}`","handlingStrategy":"type-guard","validationCode":"function assertSyncArgs(args: unknown[]): void {\n  const pending = args.find((a) => a && typeof (a as any).then === 'function')\n  if (pending) {\n    throw new Error('sync mode cannot accept a Promise arg; resolve it first')\n  }\n}\n\nassertSyncArgs(interpolatedArgs)","typeGuard":"const isThenable = (v: unknown): v is Promise<unknown> =>\n  !!v && typeof (v as any).then === 'function'","tryCatchPattern":"try {\n  $.sync`cmd ${value}`\n} catch (e) {\n  if (e instanceof Fail && /sync mode does not allow async/.test(e.message)) {\n    // fall back to async mode\n    await $`cmd ${value}`\n  } else throw e\n}","preventionTips":["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."],"tags":["sync","async","promise","template-literal","build"],"backgroundTag":null,"analyzedSha":"00a2c484e219c2e84bfc3a199febf7fbce2cfbf4","analyzedAt":"2026-08-13T02:11:06.305Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}