A failed check reverts the whole transaction and undoes every state change, refunding the unused gas. `require` validates inputs and conditions; custom errors are the cheapest way to carry a reason.
error InsufficientBalance(uint256 available, uint256 needed);
function withdraw(uint256 amount) external {
if (bal[msg.sender] < amount) revert InsufficientBalance(bal[msg.sender], amount);
require(amount > 0, "zero");
}`assert` is for invariants that should never fail — a failing assert signals a bug, not bad input.