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
- Give each fontDefinitions entry in that family a unique weight (e.g. 400 and 700)
- Or set style: 'italic' on one of the two conflicting definitions
- If you genuinely need two faces at the same weight and style, split them into two different fontFamily names
- 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
- Keep one source of truth for the font list and lint it in CI with the duplicate check above
- When reusing a variable font file for several weights, template the definitions from an array of weights so the weight always varies
- Remember style defaults to 'normal': two definitions differing only in axes still conflict
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
- Font family ${JSON.stringify(fontFamily)} declares "axes" fo
- Font family ${JSON.stringify(family.fontFamily)} declares no
- Font family ${JSON.stringify(fontFamily)} declares no weight
- Font family ${JSON.stringify(fontFamily)} declares weight ${
- ${declares}, which is not four characters. An axis tag is ex
AI-assisted analysis of expo/expo@da586c407b (2026-08-23).
Data as JSON: /api/errors/11a66c3946a6b2a6.
Report an issue: GitHub.