LEARN · DEBUGGING GUIDE

TypeORM Relation Eager vs Lazy Loading Errors: Debugging Unexpected Query Behavior

Eager and lazy loading in TypeORM can silently break your data retrieval. If one relation loads everything or nothing, the issue is likely a misconfigured decorator or circular eager loading.

IntermediateDatabase8 min read

What this usually means

TypeORM's relation loading is determined by the `eager` and `lazy` flags on the `@ManyToOne`, `@OneToMany`, `@OneToOne`, or `@ManyToMany` decorators. An eager relation automatically loads via JOIN or separate query when the parent entity is fetched. A lazy relation loads only on property access via a Promise. The most common mistake is mixing these modes inconsistently: marking a relation as eager on both sides of a bidirectional relationship causes a circular eager loading error. Another pitfall is leaving `eager: false` (default) when you expect automatic loading, leading to N+1 queries because you access the relation in a loop without explicit joins. Cache layer (e.g., Redis) can also serve stale data if relations change. TypeORM's `find` options (`relations`, `join`) override decorator settings, so a missing `relations` array can cause lazy behavior even with eager decorators.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Enable SQL logging: set `logging: true` and `logger: 'advanced-console'` in your DataSource options. Watch for multiple identical SELECTs (N+1) or unexpected JOINs.
  • 2Check all relation decorators in the affected entity and its counterpart. Use `grep -r '@ManyToOne\|@OneToMany\|@OneToOne\|@ManyToMany' src/entities/` to list them.
  • 3Verify if the relation is marked `eager: true` on both sides of a bidirectional relation – TypeORM throws at startup for this.
  • 4Inspect the `find` or `findOne` call: does it pass `relations: []`? If not, eager relations will load, others won't.
  • 5Test lazy loading by accessing the relation property and logging it: `console.log(await entity.relation)` – if it's undefined without `await`, you have a lazy loading issue.
  • 6Check if a global or custom repository overrides the decorator settings. Look for `@EntityRepository` or custom find methods that disable eager loading.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchEntity files: `src/entities/*.ts` – the decorator definitions on each relation
  • searchDataSource config: `src/data-source.ts` – check `logging` and `cache` settings
  • searchRepository or service files with `find`, `findOne`, `save` calls – look for `relations` parameter
  • searchStartup logs: TypeORM outputs errors for circular eager relations – grep for 'Circular eager relations'
  • searchDatabase query logs (if SQL logging is on): look for repeated similar SELECTs or unexpected JOINs
  • searchCache configuration: if using Redis or other cache, check `typeorm cache` settings – stale cache can hide loading errors
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningBoth sides of a bidirectional relation have `eager: true` – TypeORM throws 'Circular eager relations' error.
  • warningRelation is marked `lazy: true` but accessed without `await` in an async context – property returns undefined.
  • warningMissing `relations` array in `find` options for a non-eager relation – leads to N+1 queries when accessed.
  • warningIncorrect `join` alias in custom query builder causes missing or duplicate data.
  • warningCache layer returns stale entity that no longer has the expected relation loaded.
  • warningUsing `eager: true` on a relation that is also used with `@JoinColumn` on the owning side – no error but redundant.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildRemove `eager: true` from one side of a bidirectional relation, or if both sides need eager loading, fetch with explicit joins instead.
  • buildFor lazy relations, always use `await` when accessing the property, and ensure the entity is not detached (e.g., after `save` returns a new object).
  • buildAdd explicit `relations` array in `find` options for any relation you need to load, even if it's eager – this clarifies intent and prevents surprises.
  • buildUse `FindOptionsSelect` and `FindOptionsRelations` to fine-tune loading per query, overriding entity defaults.
  • buildDisable cache during debugging by setting `cache: false` in DataSource options, or invalidate cache after schema changes.
  • buildConvert lazy loading to eager loading if the relation is always needed, but ensure no circular dependency.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun `find` without explicit `relations` and check the SQL log – only expected queries (no N+1) should appear.
  • verifiedAccess a lazy relation property with `await` and confirm it resolves to the related entity array or object.
  • verifiedAfter fix, run the application and check for absence of 'Circular eager relations' error in startup logs.
  • verifiedWrite an integration test that fetches an entity and verifies the relation is loaded (not undefined).
  • verifiedCompare query counts before and after fix using a profiler (e.g., `typeorm-extension` or manually counting SQL logs).
  • verifiedClear cache and re-run the same operation to ensure stale data is not masking the issue.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningSetting `eager: true` on both sides of a one-to-one or many-to-many relation – TypeORM will throw at startup.
  • warningAssuming lazy loading works synchronously – always `await` or use `.then()`.
  • warningForgetting to pass `relations` when using custom repositories that bypass default find logic.
  • warningUsing `@JoinColumn` on both sides of a relation – only the owning side should have it.
  • warningIgnoring cache invalidation after altering entity definitions – old cache may still have the old relation behavior.
  • warningMixing `eager: true` with query builder joins – the eager join may conflict, causing duplicate rows.
( 07 )War story

Production N+1 Queries from Misconfigured Eager Loading

Senior Backend EngineerNode.js 18, TypeORM 0.3, PostgreSQL 14, Redis cache, Express

Timeline

  1. 09:15PagerDuty alert: API endpoint /users/:id/profile response time > 5s, p99 latency spike.
  2. 09:18Check Datadog: endpoint makes 50+ database queries per request. Normal is 2.
  3. 09:20Enable TypeORM SQL logging locally, reproduce with a test user.
  4. 09:22Log shows: 1 query to fetch User, then 50 queries to fetch Posts for that user (one per post).
  5. 09:25Inspect User entity: @OneToMany(() => Post, post => post.user, { eager: true }) – but eagar is misspelled as 'eager'.
  6. 09:27Check Post entity: @ManyToOne(() => User, user => user.posts, { eager: false }) – default.
  7. 09:30Fix typo: change 'eager' to 'eager'? Actually 'eager' is not a valid option – it's ignored, defaulting to false.
  8. 09:32Correct fix: set eager: true on the User entity's posts relation. Also add `relations: ['posts']` in find options.
  9. 09:35Deploy fix, queries reduce to 2 per request. PagerDuty alert clears.
  10. 09:45Postmortem: root cause was typo in decorator option – 'eager' instead of 'eager'. No validation by TypeORM.

I was on-call when the pager went off for high latency on the user profile endpoint. The endpoint was supposed to return a user with their posts. I checked the logs and saw that fetching one user triggered 50+ queries – classic N+1. My first thought was that lazy loading was active, but the relation was decorated with `eager: true`. Or so I thought.

I pulled up the entity file and squinted at the decorator. It said `{ eager: true }`. Wait, that's not 'eager' – it's 'eager'. TypeScript didn't complain because 'eager' is just an extra property that TypeORM ignores. The relation defaulted to lazy loading, hence the N+1 queries. I'd made a simple typo and wasted 10 minutes chasing ghosts.

After fixing the typo and adding explicit `relations` in the find options for safety, the queries dropped to two. The lesson: TypeORM doesn't validate decorator option names, so typos can silently break loading behavior. Always double-check your decorator spelling, and when in doubt, use explicit `relations` arrays to enforce the loading strategy.

Root cause

Typo in decorator option: 'eager' instead of 'eager', causing TypeORM to ignore it and fall back to lazy loading.

The fix

Corrected the decorator to `{ eager: true }` and added `relations: ['posts']` in the find options to be explicit.

The lesson

Always validate decorator option names – TypeORM does not warn about invalid properties. Use explicit `relations` in queries to override defaults and avoid N+1.

( 08 )How TypeORM Determines Relation Loading Strategy

TypeORM uses three sources to decide how a relation loads: entity decorator options, query options, and global configuration. The decorator `@OneToMany(type => RelatedEntity, inverse => inverse.relation, { eager: true, lazy: false })` sets the default. If both `eager` and `lazy` are false (default), the relation loads only if explicitly included in `relations` or `join`.

The `eager` flag triggers automatic loading via a JOIN (or separate query for collections) whenever the parent entity is fetched. The `lazy` flag returns a Promise that resolves to the related entity only when the property is accessed. These are mutually exclusive – if both are true, TypeORM throws an error. However, a common bug is setting `eager: true` on both sides of a bidirectional relation, causing a circular dependency at startup.

( 09 )Diagnosing Circular Eager Relations

If your application crashes on startup with 'Circular eager relations' error, TypeORM detected two entities that eagerly load each other. For example, `User` has `@OneToOne(() => Profile, profile => profile.user, { eager: true })` and `Profile` has `@OneToOne(() => User, user => user.profile, { eager: true })`. TypeORM cannot resolve which to join first.

To fix, remove `eager: true` from one side (usually the inverse side). Alternatively, use `FindOptionsRelations` on every query instead of eager loading. The startup error message includes the entity names, so you can grep for them and remove one eager flag.

( 10 )Lazy Loading Async Pitfalls

Lazy-loaded relations return a Promise. If you assign the entity to a variable and then access the relation without `await`, you get `Promise { <pending> }` or `undefined` if not awaited. This often happens in template engines or serialization code that expects synchronous data.

Another subtlety: after calling `save` on a new entity, the returned object may not have lazy relations initialized. You must reload the entity or explicitly access the relation within the same request context. TypeORM's lazy loading relies on the `DataSource` to remain in scope – if the entity is detached (e.g., returned from a cached query), the lazy relation property will throw 'Relation is not initialized'.

( 11 )Overriding Defaults with Find Options and Query Builder

The `find` method's `relations` option takes precedence over decorator settings. If you pass an empty array, no relations load regardless of `eager: true`. If you pass a specific relation name, only that loads. This is useful for controlling per-query but can cause confusion when you forget to include a relation you expect.

With QueryBuilder, you must explicitly call `leftJoinAndSelect` or `innerJoinAndSelect`. Eager decorators are ignored in QueryBuilder queries. This often leads to missing data if you switch from `find` to QueryBuilder without adjusting joins.

( 12 )Cache Interactions with Relation Loading

When using TypeORM's query cache (e.g., Redis), the cached result set includes the loaded relations at the time of caching. If you later change the relation loading strategy (e.g., remove eager from a decorator), the cache may still return the old eager-loaded data, masking the bug. Always clear cache after entity changes.

Additionally, if you use lazy loading with cache, the cached entity may not have the lazy relation initialized. Accessing it will throw an error. Invalidate cache or avoid caching entities with lazy relations.

Frequently asked questions

Why does TypeORM throw 'Circular eager relations' and how do I fix it?

This error occurs when two entities have a bidirectional relation with `eager: true` on both sides. TypeORM cannot determine the join order. Fix by removing `eager: true` from one side (usually the inverse side). For exampe, if User and Profile both eager-load each other, remove eager from Profile's user relation.

My lazy-loaded relation returns undefined even though data exists. What's wrong?

You likely forgot to `await` the property access. Lazy relations return a Promise. Also, ensure the entity is not detached – if you retrieved the entity from cache or a previous request, the relation may not be initialized. Reload the entity or access the relation within the same DataSource context.

Can I use both `eager: true` and `lazy: true` on the same relation?

No. TypeORM does not allow both flags to be true simultaneously. If you set both, the application will throw an error on startup. Choose one strategy: eager for always-loaded, lazy for on-demand.

Why does my `find` with explicit `relations` still cause N+1?

Check if you're accessing a nested relation that is not included in the `relations` array. For example, if you load `user` with `relations: ['posts']`, but then in code you access `user.posts.comments` without including 'posts.comments' in the relations array, that triggers extra queries. Use dot notation: `relations: ['posts', 'posts.comments']`.

How do I debug relation loading without modifying production code?

Enable TypeORM SQL logging by setting `logging: true` and `logger: 'advanced-console'` in your DataSource config. Restart the application and watch the console for query patterns. You can also use `typeorm-extension` library to log queries with execution time. For a non-intrusive approach, use database-level monitoring tools like pg_stat_statements.