expo/expo · error · Error

Font family ${JSON.stringify(fontFamily)} declares two fonts

Error message

Font family ${JSON.stringify(fontFamily)} declares two fonts for weight ${definition.weight} and style ${JSON.stringify(style)}: ${alreadyDeclaredBy} and ${definition.path}. Android matches a family on weight and style alone, so the app would crash on startup while registering it. Give each definition a weight or style of its own — "axes" cannot tell them apart.

What it means

Thrown by the expo-font Android config plugin (assertNoConflictingDefinitions, packages/expo-font/plugin/src/withFontsAndroid.ts:129) when two font definitions inside the same fontFamily claim the same weight and style pair. Android's FontFamily.Builder.addFont keys each family member by (weight, style) alone, so a duplicate pair would make the app crash on startup while registering the family — the plugin fails the prebuild instead. Note that 'axes' (fontVariationSettings) cannot disambiguate the two entries.

Source

Thrown at packages/expo-font/plugin/src/withFontsAndroid.ts:129

}

/**
 * Throws when two definitions in the same family claim the same weight and style.
 *
 * Android resolves a family by (weight, style), and `FontFamily.Builder.addFont` rejects a second
 * font carrying a pair the family already holds.
 */
export function assertNoConflictingDefinitions(fontsByFamily: GroupedFontObject) {
  for (const [fontFamily, definitions] of Object.entries(fontsByFamily)) {
    const pathByWeightAndStyle = new Map<string, string>();

    for (const definition of definitions) {
      const style = definition.style || 'normal';
      const key = `${definition.weight}/${style}`;
      const alreadyDeclaredBy = pathByWeightAndStyle.get(key);

      if (alreadyDeclaredBy) {
        throw new Error(
          `Font family ${JSON.stringify(fontFamily)} declares two fonts for weight ${definition.weight} and style ${JSON.stringify(style)}: ${alreadyDeclaredBy} and ${definition.path}. ` +
            `Android matches a family on weight and style alone, so the app would crash on startup while registering it. ` +
            `Give each definition a weight or style of its own — "axes" cannot tell them apart.`
        );
      }

      pathByWeightAndStyle.set(key, definition.path);
    }
  }
}

// https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg
const AXIS_TAG_LENGTH = 4;
const AXIS_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9]* *$/;

// Case splits the namespace: registered axes are lowercase, a font's own axes are uppercase, and a
// tag in any other case names nothing. So `SLNT` is a font's own axis, not a misspelt `slnt`.
const registeredAxisTags = ['ital', 'opsz', 'slnt', 'wdth', 'wght'];

View on GitHub (pinned to da586c407b)

Solutions

  1. Give each fontDefinitions entry in that family a unique weight (e.g. 400 and 700)
  2. Or set style: 'italic' on one of the two conflicting definitions
  3. If you genuinely need two faces at the same weight and style, split them into two different fontFamily names
  4. Re-run prebuild (npx expo prebuild --clean) after fixing to confirm the plugin passes

Example fix

// before (app.json expo-font plugin input)
{
  "fontFamily": "Inter",
  "fontDefinitions": [
    { "path": "./Inter-Var.ttf", "weight": 400, "axes": { "opsz": 14 } },
    { "path": "./Inter-Var.ttf", "weight": 400, "axes": { "opsz": 32 } }
  ]
}
// after — unique weight per definition
{
  "fontFamily": "Inter",
  "fontDefinitions": [
    { "path": "./Inter-Var.ttf", "weight": 400, "axes": { "opsz": 14 } },
    { "path": "./Inter-Var.ttf", "weight": 500, "axes": { "opsz": 32 } }
  ]
}
Defensive patterns

Strategy: validation

Validate before calling

import type { FontObject } from 'expo-font';

function assertNoDuplicateWeightStyle(fonts: FontObject[]) {
  for (const { fontFamily, fontDefinitions } of fonts) {
    const seen = new Map<string, string>();
    for (const d of fontDefinitions) {
      const key = `${d.weight}/${d.style || 'normal'}`;
      const prev = seen.get(key);
      if (prev) {
        throw new Error(`${fontFamily}: ${prev} and ${d.path} both claim weight ${d.weight} / style ${d.style || 'normal'}`);
      }
      seen.set(key, d.path);
    }
  }
}
// run before passing fonts to the plugin / app.json
assertNoDuplicateWeightStyle(fonts);

Type guard

function hasUniqueWeightStylePairs(defs: { weight?: number; style?: string; path?: string }[]): boolean {
  const keys = defs.map((d) => `${d.weight}/${d.style || 'normal'}`);
  return new Set(keys).size === keys.length;
}

Prevention

When it happens

Trigger: Passing a FontObject to useFonts (or withFonts in app.json/plugins) whose fontDefinitions contain two entries with the same weight and the same style (style defaults to 'normal'): e.g. a variable font file reused for two definitions where only 'axes' differs, or a copy-pasted definition where 'weight' was not updated. assertNoConflictingDefinitions runs during addXmlFonts at config-plugin time, so this fires on prebuild/compile, not at runtime.

Common situations: Using one variable font file (e.g. Inter[wght].ttf) to back several weights and forgetting to change weight on the copy; migrating a config from a system where variation settings distinguished faces; adding a 'display' variant with the same 400 weight as the regular face.

Related errors


AI-assisted analysis of expo/expo@da586c407b (2026-08-23). Data as JSON: /api/errors/11a66c3946a6b2a6. Report an issue: GitHub.