Common Infinite Loops in Quiz Apps: Causes and Fixes

In quiz apps the loop usually involves the question‑loading pipeline: fetch → parse → render → answer → repeat. Any break in that pipeline that fails to update the “finished” flag or the index pointer

March 12, 2026 · 6 min read · Common Issues

1. What Causes Infinite Loops in Quiz Apps

CategoryTypical TriggerWhy It Loops
State‑machine bugsTransition logic that never reaches a terminal state (e.g., if (answerCorrect) goToNext(); else stay();)The “else” branch re‑enters the same screen without advancing, so the UI thread keeps re‑rendering the same view.
Asynchronous callback misusePromises/Coroutines that resolve to the same handler repeatedly (await fetchQuestion().then(loadQuestion);)Each resolution schedules another fetch before the previous one finishes, creating an endless chain.
Recursive UI updatessetState(() => loadNextQuestion()); inside a build()/render() methodThe UI rebuild triggers loadNextQuestion(), which calls setState again, causing the framework to loop indefinitely.
Timer‑driven loopsTimer.periodic(Duration(seconds: 1), (_) => checkProgress()); where checkProgress never clears the timerThe timer never cancels because the condition that should stop it is never satisfied (e.g., if (quizFinished) timer.cancel(); but quizFinished never flips).
Incorrect loop conditionswhile (!isQuizOver) { fetchNext(); } where isQuizOver is only set inside an async callback that never runsThe loop blocks the main thread; the callback that would set isQuizOver never gets a chance to execute.
Navigation stack misuseNavigator.pushNamed(context, '/question'); inside the initState of the same screenEach navigation pushes the same route again, creating an endless stack that the user never escapes.
Data‑binding loopsTwo‑way binding between UI and model (question = model.current; model.current = question;)Updating one side triggers the other, which immediately updates the first side again.

In quiz apps the loop usually involves the question‑loading pipeline: fetch → parse → render → answer → repeat. Any break in that pipeline that fails to update the “finished” flag or the index pointer will cause the cycle to repeat forever.

---

2. Real‑World Impact

---

3. Concrete Manifestations

#SymptomUnderlying Loop Type
1The same question flashes repeatedly after tapping Submit.State‑machine bug – answer handler re‑loads the current index instead of index+1.
2Loading spinner never disappears on the Results screen.Timer‑driven loop that never clears because quizFinished never becomes true.
3App freezes for 10–30 seconds on launch, then crashes with “Stack overflow”.Recursive setState inside build() that keeps calling loadNextQuestion().
4After a network outage, the app keeps retrying the same API call every second, draining battery.Asynchronous callback that re‑invokes fetchQuestion() in its own catch block without a back‑off limit.
5Navigation back button returns to the same question screen instead of the previous menu.Navigation stack misuse – push inside initState.
6Accessibility screen reader repeats the same question text endlessly.Data‑binding loop between UI label and live‑region model.
7In “practice mode”, the “Next” button does nothing; the UI shows a busy indicator forever.While‑loop that blocks the main thread (while (!isQuizOver) { fetchNext(); }).

---

4. How to Detect Infinite Loops

  1. Automated UI exploration (SUSA)
  1. Performance profiling
  1. Log analysis
  1. Watchdog timers
  1. Static analysis
  1. Accessibility testing

---

5. Fixing the Examples

Example 1 – Re‑loading the same question


// Bad
fun onSubmit(answer: String) {
    if (isCorrect(answer)) {
        loadQuestion(currentIndex)   // <-- should be +1
    } else {
        showError()
    }
}

Fix


fun onSubmit(answer: String) {
    if (isCorrect(answer)) {
        currentIndex++
        if (currentIndex < questions.size) {
            loadQuestion(currentIndex)
        } else {
            navigateToResult()
        }
    } else {
        showError()
    }
}

Add a bounds check and increment the index before loading.

Example 2 – Spinner never stops


let timer = setInterval(() => {
  if (quizFinished) clearInterval(timer);
}, 1000);

Fix


function finishQuiz() {
  quizFinished = true;
  clearInterval(timer); // immediate cancel
}

Set the flag before the timer starts or use setTimeout for a one‑off check.

Example 3 – Recursive setState


@override
Widget build(BuildContext context) {
  loadNextQuestion();   // <-- called on every rebuild
  return Scaffold(...);
}

Fix


@override
void initState() {
  super.initState();
  loadNextQuestion();   // called once
}

Move side‑effects out of build() into initState or a FutureBuilder.

Example 4 – Aggressive retry


async def fetch_question():
    try:
        return await api.get('/question')
    except NetworkError:
        await fetch_question()   # no back‑off, no limit

Fix


import asyncio

MAX_RETRIES = 3
async def fetch_question(attempt=1):
    try:
        return await api.get('/question')
    except NetworkError as e:
        if attempt < MAX_RETRIES:
            await asyncio.sleep(2 ** attempt)   # exponential back‑off
            return await fetch_question(attempt + 1)
        raise e

Introduce a retry limit and back‑off.

Example 5 – Navigation stack misuse


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.question);
    Navigator.push(this, QuestionActivity.class); // pushes itself
}

Fix


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.question);
    // No self‑push; only push when user explicitly taps “Next”.
}

Only navigate in response to user actions, not in onCreate.

Example 6 – Live‑region binding loop


<div aria-live="polite" id="questionLabel">{{question.text}}</div>
<script>
  model.onChange = (q) => {
    document.getElementById('questionLabel').innerText = q.text;
    model.current = q; // writes back to model -> triggers onChange again
  };
</script>

Fix


model.onChange = (q) => {
  const label = document.getElementById('questionLabel');
  if (label.innerText !== q.text) {
    label.innerText = q.text;
  }
  // Do NOT write back to model here
};

Guard against redundant updates and avoid two‑way writes.

Example 7 – Blocking while‑loop


while (!isQuizOver) {
    fetchNext()
}

Fix


fun startQuiz() = lifecycleScope.launch {
    while (isActive && !isQuizOver) {
        fetchNext()
        delay(500) // give UI thread a chance to render
    }
}

Run the loop in a coroutine with a delay, or better yet, drive progression via user events rather than a tight loop.

---

6. Prevention – Catch Infinite Loops Before Release

  1. Integrate SUSA into CI/CD
  1. Unit‑test the state machine
  1. Static‑analysis rule set
  1. Watchdog in production builds
  1. Persona‑driven exploratory testing
  1. Accessibility regression
  1. Security‑oriented checks

By making automated detection, static guarantees, and persona‑driven exploration part of the development pipeline, quiz apps can eliminate infinite loops early, protect user experience, and keep revenue streams flowing.

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free