remix-run/remix · error · DataTableQueryError
hasManyThrough relation is missing through metadata
Error message
hasManyThrough relation is missing through metadata
What it means
A hasManyThrough relation requires 'through' metadata naming the join table and keys. If that metadata is absent (relation.through is undefined), the loader cannot build the join query and throws a DataTableQueryError. Typically caused by constructing the relation object incorrectly or omitting the through option.
Source
Thrown at packages/data-table/src/lib/database/relations.ts:116
let key = getCompositeKey(sourceRow, relation.sourceKey)
let matches = grouped.get(key) ?? []
let pagedMatches = applyPagination(matches, relation.modifiers.limit, relation.modifiers.offset)
if (relation.cardinality === 'many') {
return pagedMatches
}
return pagedMatches[0] ?? null
})
}
async function loadHasManyThroughValues(
database: QueryExecutionContext,
sourceRows: Record<string, unknown>[],
relation: AnyRelation,
): Promise<unknown[]> {
if (!relation.through) {
throw new DataTableQueryError('hasManyThrough relation is missing through metadata')
}
if (sourceRows.length === 0) {
return []
}
let throughRelation = relation.through.relation
let sourceTuples = uniqueTuples(sourceRows, throughRelation.sourceKey)
if (sourceTuples.length === 0) {
return sourceRows.map(() => [])
}
let throughQuery = createQuery(throughRelation.targetTable)
let throughPredicate = buildLinkPredicate(throughRelation.targetKey, sourceTuples)
if (throughPredicate) {
throughQuery = throughQuery.where(View on GitHub (pinned to 9696913134)
Solutions
- Provide complete through metadata: hasManyThrough(() => targetTable, { through: { table: joinTable, sourceKey, targetKey } }) per the API
- Verify the option key is named exactly 'through' and the value is truthy
- If you build relation objects programmatically, assert the through field exists before registration
Example fix
// before
tags: sourceTable.hasManyThrough(() => tagTable, {})
// after
tags: sourceTable.hasManyThrough(() => tagTable, {
through: {
table: () => postTagsTable,
sourceKey: 'postId',
targetKey: 'tagId',
},
}) Defensive patterns
Strategy: validation
Validate before calling
if (!relation.through) {
throw new Error(`hasManyThrough relation missing through metadata`)
} Type guard
function isCompleteHasManyThrough(relation: AnyRelation): boolean {
return relation.kind === 'hasManyThrough' && relation.through != null
} Try / catch
try {
await loadRelationsForRows(database, rows, relationMap)
} catch (error) {
if (error instanceof DataTableQueryError && /through metadata/.test(error.message)) {
// skip this relation and log configuration error
} else throw error
} Prevention
- Always pass the through option when using hasManyThrough
- Unit-test relation configuration objects for required metadata
- Prefer the builder API over hand-constructed relation objects
When it happens
Trigger: Defining hasManyThrough() without a through option, or manually creating a relation object lacking the through field; a malformed relation map entry that reaches loadHasManyThroughValues.
Common situations: Configuring many-to-many relations and forgetting the join table spec; copying a plain hasMany definition where hasManyThrough was intended; typos in the option name so through is never set.
Related errors
- MySQL database + method + () requires config-based construc
- MySQL database config requires a database name
- create({ returnRow: true }) failed to load inserted row
- Relation "' + relationName + '" is not defined for source ta
- hasManyThrough expects a through relation whose source table
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/97660f4b96812859.
Report an issue: GitHub.