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

  1. Provide complete through metadata: hasManyThrough(() => targetTable, { through: { table: joinTable, sourceKey, targetKey } }) per the API
  2. Verify the option key is named exactly 'through' and the value is truthy
  3. 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

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


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/97660f4b96812859. Report an issue: GitHub.