No, you need to represent every state. Like let’s say there are two async operations: a disk read, then a network request. When you transition, you need to know: did I start the disk read? If yes, cancel the disk read if it’s pending. Did the network request start? If it started but didn’t finish, then cancel it. So you can see that there is a huge amount of state you need to operate on that’s not directly captured by states A and B.
If you need recoverability from every state to A or B, then it’s not a 2-state (with intermediates) problem and you do need to go through the complexity of modelling all 6 states.
Also, the full state representation is not the problem, that’s easy enough to do with a reasonable type system. The problem is defining the transitions. But if you need the full transition matrix (or even any substantial subset), then you have a lot of things to do. But it’s things you’d have to do with a non-state machine based approach.
Realistically the only overhead with a sparse state machine representation is adding is the “catch-all” error branch you need to deal with any forbidden transitions. Everything else, you’d need to deal with anyway for normal operation. And you probably want the catch-all branch anyway, for debuggability when it breaks.
Yeah, I’ve wrestled with this, too. Usually I’ve compromised on having sub-states (Swift enums with associated types alleviate some of the pain), but still doesn’t feel quite right.
Finite state machines are an ideal mechanism to with concurrency if you put a concurrent queue in front of them. Such a program is easily coded with Akka's FSM trait. It even adds the possibility to trigger transition based on timer. As soon as you are in the realm of time sensitive programs, i.e. because of network requests, this is a very powerful and easy to understand way to program complicated parts of programs.