ethers v6: transaction receipt has no .events property — read receipt.logs and decode with contract.interface.parseLog
Symptom: after const receipt = await (await contract.someMethod()).wait();, receipt.events is undefined, so code that does receipt.events.find(e => e.event === 'MyEvent') either throws or silently reports no events — even though the event definitely was emitted and is visible on the explorer.
Cause: this is an ethers v5 → v6 breaking change. In v5, wait() returned a receipt with a convenience events array of parsed, contract-aware event objects with .event and .args. In v6 that array is gone. The receipt exposes readonly logs: Log[], and each Log is raw: topics, data, address, index — no decoded name, no named arguments.
Correct v6 pattern:
const receipt = await (await contract.getOwnerAddress()).wait();
for (const log of receipt.logs) {
let parsed;
try { parsed = contract.interface.parseLog(log); } catch { continue; } // not our ABI
if (parsed?.name === "OwnerReturned") console.log(parsed.args);
}parseLog returns null or throws for logs that don't match the ABI (other contracts' events in the same transaction), so guard it. If the receipt comes from a ContractTransactionResponse, ethers v6 also gives receipt.logs already typed as EventLog for logs it could decode, so log.args may be populated — check log instanceof EventLog before relying on it.
Two wrong turns to avoid: comparing log.topics[0] to a plain event name string — topics[0] is the keccak hash of the full event signature, so use contract.interface.getEvent('OwnerReturned').topicHash; and expecting a view-style return value from a state-changing call. A transaction never returns data to off-chain callers, which is exactly why the value gets emitted as an event.
Source: adapted from a Stack Exchange answer by Mila A (question by parsecer), https://ethereum.stackexchange.com/questions/161120/ethersjs-check-that-a-transaction-method-in-contract-emitted-an-event — CC BY-SA 4.0