google/zx · error · Fail
Malformed command at ${$.from}
Error message
Malformed command at ${$.from} What it means
Thrown by ProcessPromise.build() when at least one chunk of the tagged-template's `pieces` (the TemplateStringsArray) is null or undefined. In normal use pieces are always strings, so this guard catches programmatic or reflective invocation of `$` with an invalid pieces array rather than a genuine template literal. The message includes $.from (caller location) to pinpoint the call site.
Source
Thrown at src/core.ts:298
this._resolve = resolve!
this._reject = reject!
if (snapshot.halt) this._stage = 'halted'
try {
this.build()
} catch (err) {
this.finalize(ProcessOutput.fromError(err as Error), true)
}
} else ProcessPromise.disarm(this)
}
// 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 } = selfView on GitHub (pinned to 00a2c484e2)
Solutions
- Use the tagged template directly: `` $`echo hi` `` instead of constructing pieces yourself.
- If wrapping `$`, ensure the forwarded pieces array contains only strings (filter/null-coalesce).
- Inspect the {from} location in the error message to find the offending call site.
- Avoid reflective instantiation; use the public `$` API.
Defensive patterns
Strategy: type-guard
Validate before calling
function sanitizePieces(pieces: unknown[]): string[] {
return pieces.map((p) => (p == null ? '' : String(p)))
}
// if you must forward to $ programmatically:
const clean = sanitizePieces(rawPieces) Type guard
const areValidPieces = (p: unknown): p is TemplateStringsArray => Array.isArray(p) && p.every((chunk) => typeof chunk === 'string')
Try / catch
import { Fail } from 'zx'
try {
await runCustomWrapper()
} catch (e) {
if (e instanceof Fail && /Malformed command at/.test(e.message)) {
console.error('wrapper injected null/undefined into template pieces:', e.message)
}
throw e
} Prevention
- Always invoke $ as a tagged template literal — never build the pieces array by hand.
- If you wrap $, pass through the original TemplateStringsArray untouched.
- Filter null/undefined out of any dynamically constructed pieces before forwarding.
When it happens
Trigger: Calling the `$` function reflectively with a hand-built array containing null/undefined entries; a library/wrapper that intercepts and mutates the pieces array before forwarding to `$`; an edge case from a tagged-template polyfill.
Common situations: Wrapping/proxying `$` for logging or translation; dynamic command builders that construct the pieces array manually; buggy middleware that injects null placeholders.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- sync mode does not allow async command resolution
- Inappropriate usage. Apply $ instead of direct instantiation
- No shell is available: ${Fail.DOCS_URL}/shell
- Callback is required for retry
AI-assisted analysis of google/zx@00a2c484e2 (2026-08-13).
Data as JSON: /api/errors/739b82b1ae66c913.
Report an issue: GitHub.