affaan-m/ECC · error · Error
template has unfilled placeholders
Error message
template has unfilled placeholders: ${[...new Set(leftover)].join(', ')} What it means
render() substitutes {{KEY}} placeholders in the agreement template with values. After substitution, it scans for leftover {{UPPERCASE_PLACEHOLDER}} tokens; any survivors mean the values object did not cover the template. It throws listing the distinct unfilled placeholders, so drafts never ship with visible template syntax.
Solutions
- Read the error — it names each unfilled placeholder (e.g. {{EFFECTIVE_DATE}}).
- Add the missing key(s) to the values object returned by buildValues(), or fill them in your caller before rendering.
- Check whether you are passing an outdated/edited template that no longer matches the script's values; restore the matching template version.
- Verify key case matches exactly — placeholders are uppercase; substitution is split/join on exact '{{KEY}}'.
Example fix
// before
return { file: spec.file, cp, role: spec.role }; // template also uses {{NOW}}
// after
return { file: spec.file, cp, role: spec.role, now: now.toISOString().slice(0, 10) }; Defensive patterns
Strategy: try-catch
Validate before calling
function checkPlaceholders(template, values) {
let out = template;
for (const [k, v] of Object.entries(values)) out = out.split(`{{${k}}}`).join(v);
const leftover = out.match(/\{\{[A-Z_]+\}\}/g);
if (leftover) throw new Error(`unfilled: ${[...new Set(leftover)].join(', ')}`);
} Type guard
null
Try / catch
try {
const draft = render(template, values);
} catch (e) {
if (e.message.startsWith('template has unfilled placeholders')) {
console.error(`Template/values mismatch: ${e.message}`);
} else throw e;
} Prevention
- Keep template and buildValues()'s values object in sync; change them together in one commit.
- Add a smoke test that renders the shipped template with default values and asserts no leftovers.
- Never hand-edit templates without checking which {{KEYS}} they reference.
When it happens
Trigger: A values object missing a key that the template references (e.g. template uses {{CP_LONG}} but values only supplies cp); a modified/custom template with new placeholders not mirrored in values; keys in values that differ by case from the template.
Common situations: Editing the template to add a field but forgetting to add the corresponding value in buildValues()'s return object; version mismatch between template and script after an update; hand-crafted values passed directly to render().
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- launcherCommand must be a non-empty string
- Template content must be a non-empty string
- Template name must be a non-empty string
- -32602
- a claim token is required
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/7777e2e5b8ec9805.
Report an issue: GitHub.
Appendix: source
Thrown at skills/master-agreement-generator/scripts/build-agreement.js:128
FEE_CLAUSE: fill(clauses.fee),
SCHEDULE_ROWS: renderScheduleRows(spec.schedule),
SUPPLEMENT_CLAUSE: supplement,
CP_SIGBLOCK: (spec.legal || cp).toUpperCase(),
CP_SIGNER: spec.signer || BLANK,
CP_TITLE: spec.title || BLANK,
CP_EMAIL: spec.email || BLANK,
};
}
function render(template, spec, now) {
const values = buildValues(spec, now);
let output = template;
for (const [key, value] of Object.entries(values)) {
output = output.split(`{{${key}}}`).join(value);
}
const leftover = output.match(/\{\{[A-Z_]+\}\}/g);
if (leftover) {
throw new Error(`template has unfilled placeholders: ${[...new Set(leftover)].join(', ')}`);
}
return `${DRAFT_NOTICE}\n\n${output}`;
}
function pandocAvailable() {
const probe = spawnSync('pandoc', ['--version'], CONVERTER_OPTIONS);
return !probe.error && probe.status === 0;
}
function outputPaths(outDir, file) {
const root = path.resolve(outDir);
const destinations = ['md', 'docx'].map(extension => path.resolve(root, `${file} MASTER.${extension}`));
for (const destination of destinations) {
if (path.dirname(destination) !== root) {
throw new Error('spec.file must keep generated files directly inside the output directory');
}
// lstat also detects dangling links. Check BOTH outputs before the first write,
// even when conversion is disabled. The caller must control this directory;View on GitHub (pinned to 8321021c54)