DEV Community

Cover image for The Blinking Toilet Light and My `isProcessing` Flag Were Doing the Same Job
tosane932
tosane932

Posted on

The Blinking Toilet Light and My `isProcessing` Flag Were Doing the Same Job

Introduction

Hello from Japan! 🇯🇵

I am a professional truck driver teaching myself Python and web development while working toward a career transition into web engineering.

This article records what I learned after approximately 122 hours of programming study, starting on May 12, 2026.

Recently, I added a ripple animation effect to the answer buttons in my self-developed application:

🚛 DPT — Driver Personality Test

https://qiita.com/tosane932/items/220d0f7d36bd79b2aa81

At first, I thought it would be a small visual improvement.

However, while implementing it, I realized that the blinking light on my toilet control panel and a JavaScript flag named isProcessing were performing exactly the same role.

This article explains that connection.


It Started as Protection Against Repeated Clicks

In DPT, clicking an answer button moves the user to the next question.

In the original version, the next question appeared immediately after the button was clicked.

However, this created a problem.

If a user repeatedly clicked the button, the application continued advancing through the questions at the same speed.

In an extreme case, someone could finish all 50 questions in only a few seconds.

That would reduce the reliability of the personality test and could also create invalid answer records.

To prevent this, I introduced a processing-state flag.

let isProcessing = false;

testContainer.addEventListener("click", (event) => {
    if (isProcessing) return;

    const button = event.target.closest(".option-btn");

    if (!button) return;

    isProcessing = true;

    createRipple({
        currentTarget: button,
        clientX: event.clientX,
        clientY: event.clientY
    });

    const qIdx = Number(button.dataset.qIndex);
    const oIdx = Number(button.dataset.oIndex);

    setTimeout(() => {
        if (oIdx === -1) {
            handleAnswer(qIdx, -1, "No answer", 0);
        } else {
            const option =
                shuffledQuestions[qIdx].shuffledOptions[oIdx];

            handleAnswer(
                qIdx,
                oIdx,
                option.text,
                option.score
            );
        }

        isProcessing = false;
    }, 300);
});
Enter fullscreen mode Exit fullscreen mode

While isProcessing is true, every additional click is ignored.

The application waits until the current process finishes and the flag returns to false.

It is a very small mechanism, but it prevents:

  • Repeated processing
  • Duplicate answer submission
  • Unexpected question skipping
  • Invalid score records
  • UI transitions that overlap

“Wait, Is This the Same as My Toilet?” 🚽

While implementing this code, I suddenly remembered the control panel on my toilet at home.

When I press the flush button, a light begins blinking.

While the light is blinking, pressing the button again does nothing.

After approximately 15 seconds, the light stops blinking and returns to its normal state.

Only then does the system allow another flush.

The relationship looks like this:

đźš˝ Toilet Control Panel đźš› DPT Code
Press the flush button Click an answer button
The light begins blinking isProcessing = true
Additional button presses are ignored if (isProcessing) return;
Wait approximately 15 seconds Wait with setTimeout(..., 300)
The light returns to normal isProcessing = false
The system accepts another action The next click is accepted

The toilet designer does not assume:

Everyone will press the button only once and then wait patiently.

The designer must consider users who may press the button repeatedly.

That could include:

  • A child who does not understand the control panel
  • An impatient adult
  • Someone who believes the first press was not detected
  • Someone who presses several times out of habit

If the system accepted every input immediately, it could create unnecessary repeated operations or unexpected behavior.

That is why the system enters a temporary state in which additional input is ignored.

The blinking light communicates:

The system received your request and is currently processing it.

My isProcessing flag followed exactly the same idea.

Instead of designing under the optimistic assumption that:

The user will click only once.

I designed for the possibility that:

The user may click repeatedly.

I later learned that this way of thinking is closely related to defensive programming.


What Is Defensive Programming?

Defensive programming means designing software under the assumption that unexpected input or behavior may occur.

It does not mean believing that users are malicious.

It means recognizing that users may:

  • Click repeatedly
  • Enter invalid values
  • Perform actions in an unexpected order
  • Lose network connectivity
  • Reload the page during processing
  • Use the application differently from the developer's expectations

A defensive design attempts to reduce the damage caused by those situations.

In this example, the defensive line is only one statement:

if (isProcessing) return;
Enter fullscreen mode Exit fullscreen mode

But that one line prevents the same operation from running several times simultaneously.

The application does not ask the user to behave perfectly.

It protects itself.


The Ripple Animation Was More Than Decoration

Originally, I introduced the ripple animation only to improve the appearance of the buttons.

When a user taps an answer, a visual wave spreads outward from the tapped position.

However, the animation also creates a short transition period.

During that period:

isProcessing = true;
Enter fullscreen mode Exit fullscreen mode

The application temporarily blocks further input.

This means the visual effect provides two benefits.

1. Visual Feedback

The user can see that the tap was received.

Without feedback, someone may think:

Did the button actually respond?

That uncertainty may cause another click.

2. Processing Protection

The application ignores additional clicks while the transition is running.

As a result, the animation improves both:

  • User experience
  • Input reliability

What began as a visual improvement also became part of the application's safety mechanism.


đźš› Safe Following Distance Uses the Same Principle

The same structure also exists in truck driving.

Truck drivers must constantly consider how much distance the vehicle needs to stop safely.

A vehicle's total stopping distance can be described as:

Stopping distance
=
Thinking distance
+
Braking distance
Enter fullscreen mode Exit fullscreen mode

Thinking Distance

This is the distance traveled between:

  1. The driver noticing a hazard
  2. The driver deciding to brake
  3. The driver physically pressing the brake pedal

The vehicle continues moving during this period.

Braking Distance

This is the distance traveled after the brakes begin working until the vehicle comes to a complete stop.

The total stopping distance includes both.

Because trucks are larger and heavier than ordinary passenger vehicles, they generally require a greater safety margin.

That means truck drivers must leave more distance between vehicles.


Why the Margin Matters

Driving closely behind another vehicle would not always create a problem if the vehicle ahead behaved perfectly.

However, real roads contain unpredictable behavior.

For example:

  • A vehicle suddenly enters your lane
  • A driver brakes without warning
  • A vehicle stops because of an obstacle
  • A driver changes lanes without signaling
  • Traffic suddenly slows
  • A pedestrian or cyclist enters the road

A professional driver cannot design a safety strategy based on:

The other driver will probably behave correctly.

Instead, we assume:

The other vehicle may do something unexpected.

The following distance is a physical safety margin.

It creates time and space to respond when reality does not match expectations.

The isProcessing flag creates a similar margin in software.

It gives the current operation time to finish before accepting another one.


Weather Changes the Required Margin

On rainy days, water enters the space between the road surface and the tires.

In severe cases, this can contribute to hydroplaning, where the tires lose effective contact with the road.

As road conditions become worse, braking performance may decrease.

Even at the same speed, the required braking distance can increase.

That means the safety margin must also increase.

Software systems have changing conditions too.

For example:

  • A slow device
  • A slow network
  • A delayed API response
  • Heavy browser processing
  • Multiple rapid clicks
  • Animation timing differences

A system that works perfectly under ideal conditions may behave differently under stress.

Defensive programming means leaving enough margin for those non-ideal conditions.


Toilet, JavaScript, and Truck Driving

The three examples can be summarized like this:

đźš˝ Toilet Designer

Assumes:

The user may press the button repeatedly.

Protection:

Ignore additional input while the system is processing.

đź’» JavaScript Developer

Assumes:

The user may click the answer button repeatedly.

Protection:

if (isProcessing) return;
Enter fullscreen mode Exit fullscreen mode

đźš› Truck Driver

Assumes:

Another vehicle may move unexpectedly.

Protection:

Maintain enough following distance to respond safely.

The environments are completely different.

However, the underlying design principle is the same:

Do not assume that other people or systems will behave exactly as expected.

Instead, build in:

  • Margins
  • Guards
  • Temporary locks
  • Validation
  • Safe defaults
  • Recovery time

Optimistic Design vs Defensive Design

An optimistic design might say:

testContainer.addEventListener("click", (event) => {
    handleAnswer();
});
Enter fullscreen mode Exit fullscreen mode

This assumes every click is intentional and properly timed.

A defensive version asks:

  • Is the system already processing?
  • Was an actual answer button clicked?
  • Are the data attributes valid?
  • Does the selected question exist?
  • Has this answer already been submitted?

For example:

testContainer.addEventListener("click", (event) => {
    if (isProcessing) return;

    const button = event.target.closest(".option-btn");

    if (!button) return;

    const qIdx = Number(button.dataset.qIndex);
    const oIdx = Number(button.dataset.oIndex);

    if (!Number.isInteger(qIdx)) return;
    if (!Number.isInteger(oIdx)) return;
    if (!shuffledQuestions[qIdx]) return;

    isProcessing = true;

    try {
        processSelectedAnswer(qIdx, oIdx);
    } finally {
        isProcessing = false;
    }
});
Enter fullscreen mode Exit fullscreen mode

The exact implementation depends on the application.

The important point is the mindset:

Check what may go wrong before trusting the input.


One Important Weakness in My Original Code

The original implementation resets the flag only inside the delayed callback.

setTimeout(() => {
    handleAnswer(...);
    isProcessing = false;
}, 300);
Enter fullscreen mode Exit fullscreen mode

If handleAnswer() unexpectedly throws an error, the line:

isProcessing = false;
Enter fullscreen mode Exit fullscreen mode

may never run.

The interface could remain permanently locked.

A safer implementation would use try...finally.

setTimeout(() => {
    try {
        if (oIdx === -1) {
            handleAnswer(qIdx, -1, "No answer", 0);
        } else {
            const option =
                shuffledQuestions[qIdx].shuffledOptions[oIdx];

            handleAnswer(
                qIdx,
                oIdx,
                option.text,
                option.score
            );
        }
    } finally {
        isProcessing = false;
    }
}, 300);
Enter fullscreen mode Exit fullscreen mode

The finally block runs whether the processing succeeds or throws an error.

This makes it less likely that the interface will remain locked forever.

That is another example of defensive programming:

Do not only protect the normal path.

Also protect the recovery path.


A Boolean Flag Is Useful, but Not Always Enough

For this small application, a boolean flag is simple and effective.

let isProcessing = false;
Enter fullscreen mode Exit fullscreen mode

However, larger applications may need a more explicit state model.

For example:

const AppState = {
    IDLE: "idle",
    ANIMATING: "animating",
    SAVING: "saving",
    COMPLETE: "complete",
    ERROR: "error"
};

let currentState = AppState.IDLE;
Enter fullscreen mode Exit fullscreen mode

Then the click handler can check the current state.

if (currentState !== AppState.IDLE) return;
Enter fullscreen mode Exit fullscreen mode

This approach becomes useful when the application has several different processing states.

A boolean can represent only two conditions:

true
false
Enter fullscreen mode Exit fullscreen mode

A state model can explain what the application is actually doing.

For DPT, isProcessing is currently sufficient.

However, learning this distinction helped me understand how a simple guard can later evolve into state management.


What I Learned

I thought I was only adding an animation.

Instead, I learned about:

  • Preventing duplicate processing
  • Input locking
  • Visual feedback
  • Defensive programming
  • Processing states
  • Safety margins
  • Recovery with try...finally
  • The relationship between real-world systems and software design

The surprising part was that the idea was already familiar to me.

I had seen it in:

  • A toilet control panel
  • Truck following-distance management
  • Daily workplace safety procedures

I simply did not know the programming terminology.


Conclusion

The blinking toilet light and the JavaScript isProcessing flag perform the same essential role.

They both communicate:

The system is already handling your request.

Please wait before trying again.

The safety distance maintained by a truck driver follows the same broader principle:

Something unexpected may happen, so create enough margin before it does.

The common structure is:

  1. Expect unexpected behavior
  2. Prevent overlapping operations
  3. Provide clear feedback
  4. Leave enough time or space for recovery
  5. Do not depend on perfect user behavior

What began as a small animation improvement ultimately protected the reliability of the application's input.

It also helped me connect programming with mechanisms I had already encountered in daily life and professional driving.

I am learning programming without traditional textbooks, mainly through conversations with AI.

For me, breaking down everyday questions such as:

Why was this designed this way?

may be one of the most effective ways to understand software engineering.

I hope this article is useful to someone else learning independently.


Related Article

https://qiita.com/tosane932/items/220d0f7d36bd79b2aa81

My Qiita Profile

https://qiita.com/tosane932

Top comments (0)