Skip to Content
Course content

202: Practice Exercise: Building a Simple State Machine

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a piece of code I encountered in a code review a few months ago. A junior dev was building a simple order processing system. They used a string to track the order status and a function to update it. On the surface, it looked clean, but it had a massive logical hole.

let orderStatus = 'pending';

function updateStatus(newStatus) {
  console.log(`Changing status from ${orderStatus} to ${newStatus}`);
  orderStatus = newStatus;
}

// This works fine
updateStatus('paid'); 
// This also works fine
updateStatus('shipped'); 

// Oops. Someone calls this by mistake or via a weird API edge case:
updateStatus('pending'); 
console.log(orderStatus); // 'pending'

The Problem with "Open" State Transitions

The bug here isn't a syntax error; it's a logic error. In a real-world application, an order that has already been 'shipped' should never be able to jump back to 'pending'. If that happens, you've got a nightmare on your hands—refunds might trigger twice, or shipping labels might be regenerated for an item already on a truck.

The problem is that the state is "open." The updateStatus function blindly trusts whatever input it gets. When your application grows to ten or twenty possible states, trying to manage these rules with a giant if/else block inside your update function becomes a maintenance disaster. You'll end up with "spaghetti logic" where you're constantly adding checks like if (status !== 'shipped' && status !== 'delivered').

Implementing a Transition Map

The professional way to handle this is to move the logic out of the function and into a configuration object—a state machine. Instead of asking "What is the new state?", we ask "Is this transition allowed from the current state?"

const OrderMachine = {
  initial: 'pending',
  states: {
    pending: {
      PAY: 'paid',
      CANCEL: 'cancelled'
    },
    paid: {
      SHIP: 'shipped',
      REFUND: 'pending'
    },
    shipped: {
      DELIVER: 'delivered'
    },
    delivered: {
      // No outgoing transitions; this is a final state
    },
    cancelled: {
      // No outgoing transitions
    }
  }
};

let currentState = OrderMachine.initial;

function transition(action) {
  const possibleTransitions = OrderMachine.states[currentState];
  const nextState = possibleTransitions ? possibleTransitions[action] : null;

  if (nextState) {
    console.log(`Transitioning from ${currentState} via ${action} to ${nextState}`);
    currentState = nextState;
  } else {
    console.error(`Invalid action ${action} for current state ${currentState}`);
  }
}

transition('PAY');    // Success: pending -> paid
transition('SHIP');   // Success: paid -> shipped
transition('PAY');    // Error: Invalid action PAY for current state shipped

I prefer this approach because the OrderMachine object acts as a single source of truth. If the business requirements change—say, we now allow 'shipped' orders to be 'cancelled'—I don't have to hunt through my functions for if statements. I just add CANCEL: 'cancelled' to the shipped state object. It makes the code self-documenting and nearly impossible to put into an illegal state.




📋 Practical Task

Build a Media Player State Controller

Your task is to build a state machine that manages a music player. The player can be in one of four states: stopped, playing, paused, and buffering.

Implement a transition map and a handleAction function that enforces the following rules:

  • From stopped, you can only play (moves to buffering).
  • From buffering, you can only resolve (moves to playing) or stop (moves to stopped).
  • From playing, you can pause (moves to paused) or stop (moves to stopped).
  • From paused, you can resume (moves to playing) or stop (moves to stopped).

Test your machine by attempting a sequence of valid moves, and then attempt an invalid move (like trying to resume while the state is stopped) to ensure your error handling catches it.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.