When Data Changes
State is the collection of values a program holds at a given moment. Mutation is changing that state — reassigning a variable, adding to a list, updating a field. Mutation is powerful and often necessary, but it is also the main reason programs become hard to reason about: to know what a piece of code does, you must know the current state, which earlier code may have changed.
A side effect is any observable change a function makes beyond returning a value: modifying a global variable, mutating an argument, printing to the screen, writing a file. A function without side effects, whose output depends only on its inputs and which changes nothing outside itself, is called pure. Pure functions are easy to reason about and test: the same inputs always give the same outputs, every time.
A subtle trap arises with shared references. When two names refer to the same mutable object (a list, say), mutating it through one name changes what the other sees too — an aliasing bug that surprises programmers who expected an independent copy. Distinguishing "same object" from "equal-but-separate copy" is essential.
The practical wisdom: prefer pure functions and limit mutation to where it is genuinely needed, and keep mutable state contained. Code that changes little hidden state is far easier to understand, test, and parallelize.
Common pitfall: assuming that assigning one variable to another (b = afor a list) makes an independent copy. In many languages it makes both names refer to the same object, so mutatingbalso changesa. To get an independent copy you must explicitly copy; otherwise you have two aliases of one shared, mutable object.
When You Do Not Control the Order
Every program so far has run top to bottom: your code decided what happened next. An event-driven program inverts that. You register handlers (also called callbacks) — code to run when a button is clicked, a packet arrives, a timer expires — and then hand control to an event loop, which waits for events and calls whichever handler matches. Your code no longer chooses the order; the outside world does.
This is the natural shape for user interfaces, servers and embedded control, and it sharpens everything else in this lesson. Handlers cannot pass values to one another, so they communicate almost entirely through shared state: one writes a field, a later one reads it. Every warning about mutation applies with more force here, because you cannot tell by reading the code which handler ran before which.
Two traps follow directly. A handler that takes a long time blocks the loop — no other event is serviced until it returns, which is exactly why a frozen interface is the classic symptom of work done in the wrong place. And a handler that causes an event of its own risks re-entrancy: being entered a second time before the first call has finished, finding the state half-updated.
The discipline is the one already stated, applied harder. Keep handlers short, keep the state they share small and explicitly owned, and assume no ordering beyond what the system actually guarantees.
Common pitfall: assuming handlers run in the order their events were caused, or that one finishes before another starts. Neither is generally promised. Logic written on the belief that "the click handler always runs before the timer handler" works until the day it does not, and the resulting failure is intermittent and very hard to reproduce.