What this usually means
TypeScript's strictNullChecks mode treats null and undefined as distinct types. When you see 'Object is possibly undefined', the compiler has detected a code path where a value might not have been assigned or might be explicitly null/undefined. The non-obvious part is that this often arises from control flow analysis limitations — TypeScript can't always track assignments across async boundaries, callbacks, or complex conditional chains. It might also come from generic types that don't carry nullability constraints, or from optional properties that aren't properly narrowed.
The first ten minutes — establish facts before touching code.
- 11. Run `tsc --noEmit --strictNullChecks` to see all errors at once.
- 22. Locate the exact line and column. Use `// @ts-expect-error` temporarily to isolate.
- 33. Check if the variable is declared with `let` or `var` — TypeScript's control flow analysis may not narrow across function calls or async operations.
- 44. Look for optional chaining (`?.`) in the expression — ensure the chain doesn't terminate early.
- 55. Verify the type of the variable — hover in your IDE or use `declare const` to inspect the inferred type.
- 66. If the variable comes from a generic function, check if the generic parameter has a constraint that excludes undefined (e.g., `T extends object` vs `T = any`).
The specific files, logs, configs, and dashboards that usually own this bug.
- searchtsconfig.json — check `strictNullChecks` is `true` (or `strict` includes it).
- searchThe variable declaration — is it `let`? TypeScript may not narrow across re-assignments.
- searchAsync function bodies — after `await`, TypeScript resets control flow narrowing; consider using `const` or a local `const` copy.
- searchGeneric utility types — `Partial<T>`, `Pick<T, K>` may introduce `undefined` for optional properties.
- searchCallback parameters — e.g., `Array.filter` doesn't narrow the result type; use a type guard.
- searchFunction return types — ensure they explicitly include `| undefined` if the function can return undefined.
Practical causes, not theory. These are the things you will actually find.
- warningUsing `let` instead of `const` for variables that are assigned once, causing TypeScript to forget narrowing after the first assignment.
- warningAsync/await — after an `await`, TypeScript resets control flow analysis, so a narrowed type becomes broad again.
- warningGenerics that don't enforce non-nullable constraints — e.g., `T` without `extends object` may allow `undefined`.
- warningOptional chaining used incorrectly — `obj?.prop` returns `undefined` if `obj` is null/undefined, but subsequent access may still be unsafe.
- warningArray methods like `filter` or `find` — `Array.filter(Boolean)` doesn't narrow the type; use a custom type guard.
- warningThird-party library types that are not strict — some `@types` packages may not have accurate nullability annotations.
Concrete fix directions. Pick the one that matches your root cause.
- buildReplace `let` with `const` where possible, or use definite assignment assertion `!` only after verifying the logic.
- buildIn async functions, capture the awaited value in a `const` variable before using it in conditionals.
- buildAdd explicit null checks with early returns or `if (x == null) throw new Error('x is null')` to narrow the type.
- buildUse optional chaining (`?.`) and nullish coalescing (`??`) to provide defaults and avoid access on null.
- buildFor generic functions, add `extends object | null` or use `NonNullable<T>` to remove undefined from the type.
- buildWrite custom type guards (e.g., `function isDefined<T>(x: T | undefined): x is T { return x !== undefined; }`) to narrow array results.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun `tsc --noEmit` again and confirm zero errors.
- verifiedCheck that the fix doesn't break runtime behavior — if you used `!`, ensure the value is truly never undefined at that point.
- verifiedWrite a unit test that covers the edge case (e.g., null input) to ensure your handling works.
- verifiedUse TypeScript's `--strict` mode to catch any remaining issues.
- verifiedReview the generated JavaScript to ensure no runtime null reference errors are possible.
- verifiedAdd a lint rule like `@typescript-eslint/strict-boolean-expressions` to prevent similar issues.
Things that make this bug worse or harder to find.
- warningUsing non-null assertion `!` everywhere — it suppresses the error but doesn't fix the root cause and can lead to runtime crashes.
- warningAdding `// @ts-ignore` — this hides the error and will fail under `strict` mode.
- warningIgnoring the error in callbacks or async functions — these are the most common places for actual runtime bugs.
- warningAssuming `filter(Boolean)` narrows the type — it doesn't; you need a user-defined type guard.
- warningSetting `strictNullChecks: false` — you lose all the safety net for null/undefined errors.
- warningOver-using optional chaining without handling the undefined case — it just shifts the error downstream.
Production crash due to unhandled undefined in async user fetch
Timeline
- 09:00User reports blank screen on profile page after login.
- 09:15Check Sentry: 'Cannot read properties of undefined' on user.name.
- 09:20Identify component: ProfileHeader uses user from useAppSelector.
- 09:25Review code: user is from Redux store, initialized as null.
- 09:30tsconfig has strictNullChecks: true, but no compilation errors.
- 09:35Discover that the component uses non-null assertion (!) on user.
- 09:40Reproduce: clear Redux state, re-login, user is null for a frame.
- 09:45Fix: replace ! with optional chaining and default empty object.
- 09:50Deploy fix, confirm Sentry error stops.
We started getting reports of a blank screen on the profile page right after login. Sentry showed 'Cannot read properties of undefined (reading 'name')' on ProfileHeader. The stack trace pointed to a line where we accessed user.name. I checked the component and saw user was from the Redux store with a non-null assertion `user!`. The code compiled fine with strictNullChecks because the assertion silenced the error.
I reproduced by clearing Redux state and logging in again. The store initially sets user to null before the API call completes. That one frame where user was null caused the crash. The non-null assertion was a band-aid that hid the real issue.
The fix was to remove the assertion and use optional chaining: `user?.name ?? 'User'` and a guard that returns null early if user is null. I also added a check in the selector to return a default object. After deployment, the error stopped. The lesson: non-null assertions are a code smell – they often mask real race conditions.
Root cause
Non-null assertion (`!`) on a Redux state value that can be null during the initial render before the async fetch completes.
The fix
Replaced `user!` with optional chaining and added a null guard. Also updated the TypeScript types to reflect that user can be null initially.
The lesson
Never use non-null assertion without understanding why the value could be undefined. Enable strictNullChecks and treat every 'possibly undefined' error as a potential runtime crash.
TypeScript's control flow narrowing works well within synchronous code. For example, after an `if (x)` check, TypeScript knows x is truthy. However, this narrowing is lost across async boundaries. After an `await`, TypeScript resets the narrowed type to the declared type. This is a common source of 'possibly undefined' errors.
Consider this: `let user = getUser(); if (user) { await something; console.log(user.name); }` — after await, `user` is back to `User | undefined`. To avoid this, assign to a `const` before the await: `const user = getUser(); if (user) { const userCopy = user; await something; console.log(userCopy.name); }`.
When writing generic functions, TypeScript may infer `undefined` as a valid type for the generic parameter if no constraints are given. For example, `function identity<T>(x: T): T { return x; }` — `T` can be `undefined`. If you then access a property on the result, you'll get an error.
Fix by adding constraints like `T extends object` or use the built-in `NonNullable<T>` to remove null/undefined. Alternatively, use overloads or conditional types to handle nullable inputs explicitly.
`Array.filter(Boolean)` does not narrow the resulting type in TypeScript. The type remains `(T | undefined | null)[]`. To narrow, you need a user-defined type guard: `function isDefined<T>(x: T | undefined): x is T { return x !== undefined; }`.
Similarly, `Array.find` returns `T | undefined`. If you know the element exists, use `!` only after a check, or use `find` with a type guard that returns `x is T`.
Frequently asked questions
Why does TypeScript say 'Object is possibly undefined' even though I checked for null?
This often happens because the check is not in the same scope or after an async operation. TypeScript's control flow analysis is flow-sensitive but resets at certain boundaries like function calls, `await`, or when a variable is reassigned. Use `const` or a local copy to preserve the narrowed type.
Can I use `!` to fix the error permanently?
No. The non-null assertion `!` tells TypeScript to ignore the possibility of null/undefined, but it doesn't change runtime behavior. If the value is actually null, you get a runtime error. Use it only when you are absolutely certain the value is not null, and document why. Prefer optional chaining and null checks.
Why does `filter(Boolean)` not narrow the type?
`Boolean` as a callback returns a boolean, not a type guard. TypeScript does not infer that the filter removes falsy values from the type. You need to provide a type guard: `arr.filter((x): x is MyType => x != null)`.
How do I handle 'possibly undefined' in chained optional properties?
Use optional chaining `?.` throughout the chain, and provide a default with `??` or a fallback. For example: `data?.user?.name ?? 'Unknown'`. Ensure every intermediate step can be undefined.
Does enabling strictNullChecks affect performance?
Only at compile time – it may increase type-checking time slightly, but it has no impact on runtime performance. The benefit of catching null-related bugs far outweighs the compile-time cost.