Meet Tim. Tim is an architect (as in hard hat), and he was tasked with rebuilding the main staircase in a mansion renovation project. He's barely slept in the last 4 days, but he finally has the blueprints for what he thinks is the best damn staircase his firm has ever seen. He races to the construction site, a huge smile across his face, and hands the plans to the contractor. The contractor, being an overall polite person, asks Tim to visualize going up or down the stairs he's proposing to build. His smile now fading and his face red with embarrassment, Tim mumbles something about copying the plans from StackOverflow and goes back to the drawing board.
These are the blueprints Tim gave the contractor:
The stairs we see here are called Penrose stairs and they're basically a bug. They're describing an object that cannot exist in 3D space. You can go up or down these stairs for as long as you want, but you'll never get higher or lower.
Another example of this is a Penrose triangle:
The reason Tim was able to create this impossibility is because a 2D medium was used to represent a 3D object. If Tim would have built a model of the staircase rather than sketching it, he would never have been able to get this result, since the medium itself prevents it.
How can I avoid Tim's fate?
In software development we also have things that act like dimensions, for example: typed vs untyped code, manual memory management vs a borrow checker + lifetime annotations, and state machines vs (normal) reducer based state management (henceforth "flat" state). In the same way a model would have forced Tim to build his staircase differently and confront the fact that Penrose stairs are not physically possible, a state machine regulates when you can use which action, and prevents you from reaching an invalid state by sending an action at the wrong time. Type annotations will prevent you from "putting a square peg in a round hole" (passing type A to a function expecting type B) but untyped code will happily do as it's told causing a bug or a crash. Lifetime annotations will prevent you from using a variable that has already been freed and doesn't live long enough, while low level languages without lifetime annotations will warn you by crashing in production when you're on vacation.
Selecting the right medium to work with (with the correct dimensions for your requirement / logic) can make a huge difference in the amount of bugs you encounter and how easy it is to change / extend your project.
Don't let Tim's sacrifice be in vain!