- Define custom errors with descriptive names and relevant parameters: `error InsufficientBalance(uint256 available, uint256 required);` — they cost less gas than `require` with strings.
- Use `revert CustomError(param)` instead of `require(condition, "string")` for all validation that needs to communicate failure context.
- Use `assert()` only for invariants that should never fail in correct code — it consumes all remaining gas on failure unlike `require`.
- Group related custom errors at the top of the contract file alongside the events they mirror for discoverability.
- Use `require` for input validation and access control checks at the function entry point — validate all external inputs before any state changes.
- Wrap external contract calls in try/catch blocks when a failed call should not revert the entire transaction but trigger fallback logic.
- Use `error Unauthorized(address caller, address required)` patterns to log exactly who called a function and what address was expected for debugging.