Consider two clients, Alice and Bob, they are both connecting to some IP 10.20.30.40 using TLS or QUIC.
Alice wants to access naughty.thing.example and Bob wants to access bland.stuff.example. Perhaps Mallory is trying to prevent Alice from accessing naughty.thing.example (but they don't mind bland.stuff.example) or perhaps Alice just doesn't want Eve to know what she's accessing. Mallory and Eve are both on the network, able to interpose between Alice and Bob and 10.20.30.40 and neither Alice nor Bob can easily prevent that.
Under domain fronting, Bob is just honest, he tells the server I want bland.stuff.example, and then he uses bland.stuff.example as usual and everything works. This means for technical operations it's OK if the 10.20.30.40 server cares what Bob said during connection. For example maybe once Bob says bland.stuff.example, the server spins up an IPC to a Python server which only knows about bland.stuff.example and splices Bob's connection to the IPC.
Under domain fronting, Alice's situation is tricky. Alice says she wants bland.stuff.example but she actually uses naughty.thing.example and expects that to work. Technical ops people who've arranged that bland.stuff.example connections get spliced are faced with bug reports - why didn't naughty.thing.example work, 10.20.30.40 was the right server ???
Under ECH, both Alice and Bob are presenting a visible name (which might be bland.stuff.example or some other value entirely) which Mallory and Eve can easily read, but they're also providing an encrypted destination, Alice can encrypt naughty.thing.example while Bob encrypts bland.stuff.example† The 10.20.30.40 server can decrypt the name, and thus it knows which service Alice and Bob actually intend to access.
† If you thought "What about padding? Those names are different lengths" congratulations, you're now at step 1 of a long process which is why this was not trivial to design and implement.