# What Happens When a Token Reference in DESIGN.md Is Broken?

> Learn what happens when a token reference in DESIGN.md is broken. Discover how the linter handles unresolved references and preserves your design system state.

- Repository: [Google Labs Code/design.md](https://github.com/google-labs-code/design.md)
- Tags: deep-dive
- Published: 2026-07-04

---

**When a token reference in DESIGN.md cannot be resolved, the linter emits a specific error diagnostic, omits the broken reference from the concrete design system state, and preserves the raw token string in the affected component properties while tracking it in an unresolved references array.**

The [`google-labs-code/design.md`](https://github.com/google-labs-code/design.md/blob/main/google-labs-code/design.md) repository provides a CLI linter that parses DESIGN.md files into a structured `DesignSystemState`. Understanding how this linter handles broken token references is essential for debugging design system configurations and maintaining valid token resolution chains across your components.

## How the Linter Detects and Resolves Token References

The core resolution logic resides in [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts), where the linter builds a symbol table of all token definitions before attempting to resolve cross-references during the model phase.

### Token Detection Phase

During parsing, every leaf value undergoes inspection via `isTokenReference()`, which checks for the `{section.token}` pattern. When the linter encounters a string matching this pattern in sections like `colors`, `spacing`, or `rounded`, it stores the raw reference in a symbol table for deferred resolution:

```typescript
if (typeof raw === 'string' && isTokenReference(raw)) {
  // Store raw reference for later resolution
  symbolTable.set(`colors.${name}`, raw);
}

```

*Source:* [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts) lines 58-60.

### Reference Resolution with Chain Following

After collecting primitive tokens, the linter walks the symbol table and attempts resolution via `resolveReference()`. This function follows chains of references while detecting circular paths and respecting the `MAX_REFERENCE_DEPTH` limit to prevent infinite recursion:

```typescript
const resolved = resolveReference(symbolTable, raw.slice(1, -1), new Set());
if (resolved !== null && typeof resolved === 'object' && 'type' in resolved …

```

*Source:* [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts) lines 31-35.

## Three Outcomes of a Broken Token Reference

When `resolveReference()` returns `null`—indicating the target token does not exist—the linter triggers a specific failure sequence with three distinct outcomes that ensure graceful degradation.

### 1. Omission from the Concrete Token Map

If resolution fails, the token entry is **not** promoted to the resolved maps (`colors`, `spacing`, or `rounded`). The raw reference remains in the internal symbol table but never reaches the final `DesignSystemState` concrete values, effectively excluding undefined tokens from the compiled design system output.

### 2. Literal String Preservation in Components

Components referencing broken tokens receive the raw reference string unchanged rather than a resolved value. The linter pushes the broken reference into the component's `unresolvedRefs` array while storing the literal string in the properties map:

```typescript
const resolved = resolveReference(symbolTable, refPath, new Set());
if (resolved !== null) {
  properties.set(propName, resolved);
} else {
  unresolvedRefs.push(rawValue);
  properties.set(propName, rawValue);
}

```

*Source:* [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts) lines 93-100.

### 3. Error Diagnostic Emission

The linter records the failure in the `findings` array as an error-level diagnostic attached to the specific property path. This enables precise error reporting in the CLI output:

```typescript
findings.push({
  severity: 'error',
  path: `colors.${name}`,
  message: `'${raw}' is not a valid color…`,
});

```

*Source:* [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts) lines 66-70.

## Practical Examples: Valid vs. Broken References

### Valid Token Resolution

Consider a DESIGN.md with properly defined tokens:

```markdown

# DESIGN.md

colors:
  primary: "#ff0000"
components:
  button:
    background: "{colors.primary}"

```

Running the validation:

```bash
design-cli lint examples/totality-festival/DESIGN.md

```

Output:

```

✔ 0 errors, 0 warnings

```

The resulting `DesignSystemState` contains `components.button.background` as a resolved color object with the value `#ff0000`.

### Broken Token Reference Handling

When referencing a non-existent token:

```markdown

# DESIGN.md

colors:
  primary: "{colors.nonexistent}"
components:
  button:
    background: "{colors.primary}"

```

The CLI output identifies the specific failure:

```

✖ 1 error
  colors.primary – '{colors.nonexistent}' is not a valid color. Expected a CSS color value…

```

The generated state preserves the raw string while tracking the failure:

```typescript
components.get('button')!.properties.get('background') // → "{colors.primary}"
unresolvedRefs: [ "{colors.nonexistent}" ]

```

## Summary

- **Graceful degradation**: Broken token references do not crash the parser; the linter continues processing the remaining DESIGN.md content without throwing exceptions.
- **Triple recording mechanism**: Failed resolutions result in omission from concrete token maps, literal string preservation in component properties, and population of the `unresolvedRefs` array.
- **Precise diagnostics**: The `findings` array receives error-level entries with specific paths and messages, enabling targeted debugging in [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts).
- **Downstream flexibility**: By preserving raw reference strings in component properties, downstream tools can choose to ignore broken references or surface them as additional warnings.

## Frequently Asked Questions

### Does a broken token reference crash the DESIGN.md parser?

No, the parser handles broken references gracefully. According to the implementation in [`google-labs-code/design.md`](https://github.com/google-labs-code/design.md/blob/main/google-labs-code/design.md), the `resolveReference()` function returns `null` for unresolvable tokens, triggering the error recording logic without throwing exceptions or halting the linting process.

### Where does the error message appear when a token reference is broken?

The error appears in the linter's `findings` array, which is output by the CLI at the end of the lint run. Each error includes the specific path (such as `colors.primary`), the severity level set to `'error'`, and a descriptive message indicating which token reference could not be resolved.

### Can a component still use a property that references a broken token?

Yes, the component property retains the raw reference string (e.g., `"{colors.nonexistent}"`) as its value. While the token remains unresolved in the concrete design system state, the literal string is stored in the component's properties map and tracked in the `unresolvedRefs` array for that component.

### How does the linter prevent infinite loops with circular token references?

The `resolveReference()` function tracks visited references using a `Set` passed as an argument, and it enforces a maximum recursion depth via `MAX_REFERENCE_DEPTH`. If the resolver detects a circular path or exceeds the depth limit, it aborts the resolution attempt and treats the reference as broken.