> You shouldn’t, because no code of sufficient complexity is flawless.
So why build an upgrade procedure at all, when you don't have to?
> On the Ethereum blockchain (for instance) storage and smart contracts are tightly coupled.
Sounds like an unforced error on these smart contracts' authors parts, and should not be used as an excuse to compromise the principle of code immutability.
If the possibility existed that they need to replace the business logic at a later date, then they should factor the storage logic so as to avoid this coupling. It can be done -- for example, a smart contract dapp can leverage a shared contract that only implements a public key/value store, where the keys are prefixed by the calling contract's address. Then, when the business logic contract changes, it can access any old versions' state with the old versions' contract address, apply any migrations on-the-fly, and store new state that will not overwrite old state due to this key prefixing.
In the Stacks blockchain (which I work on), we go one step further by making it so you can run an arbitrary read-only code snippet on the state of the blockchain at any point in the past (as given by a block hash). Then, you don't even need to do any migrations -- you can just query the historic state of your old contract and only store new data once it's necessary.