Calling latestRoundData() and using answer directly works in a demo. In production it's one of the most common findings in audits, because a price feed can return a value that is stale, scaled differently than you assumed, or reported while an L2 sequencer was down. A safe read checks all three.
What the feed actually returns
Chainlink Data Feeds implement AggregatorV3Interface:
function latestRoundData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
function decimals() external view returns (uint8);
answeris a signed integer scaled bydecimals(). It is not in 18 decimals unless the feed says so.updatedAtis when the answer was last updated. This is what you check for staleness.answeredInRoundis deprecated. Don't build checks on it.latestAnswer()is also deprecated and gives you no timestamp. Don't use it.
Why feeds go stale
A feed doesn't update every block. It updates when the off-chain price moves more than its deviation threshold, or when the heartbeat interval passes with no update. Both values are set per feed and per network, and they're listed on the feed's page at data.chain.link.
So "the last update was 40 minutes ago" can be perfectly normal for one feed and a clear failure for another. Your staleness limit must come from that specific feed's heartbeat, with a small margin, not a global constant. If the oracle network or the chain has problems, updatedAt stops moving and your contract should notice.
Decimals: never assume 8
Many USD-quoted feeds use 8 decimals and many ETH-quoted feeds use 18, but that's a pattern, not a rule. Read decimals() (once, at deployment is fine, since it doesn't change for a given feed) and normalize before doing math with token amounts that have their own decimals. Mixing an 8-decimal price with an 18-decimal amount without scaling is an easy way to be off by a factor of 10^10.