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
1. What Causes Infinite Loops in Quiz Apps
| Category | Typical Trigger | Why It Loops |
|---|---|---|
| State‑machine bugs | Transition 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 misuse | Promises/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 updates | setState(() => loadNextQuestion()); inside a build()/render() method | The UI rebuild triggers loadNextQuestion(), which calls setState again, causing the framework to loop indefinitely. |
| Timer‑driven loops | Timer.periodic(Duration(seconds: 1), (_) => checkProgress()); where checkProgress never clears the timer | The 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 conditions | while (!isQuizOver) { fetchNext(); } where isQuizOver is only set inside an async callback that never runs | The loop blocks the main thread; the callback that would set isQuizOver never gets a chance to execute. |
| Navigation stack misuse | Navigator.pushNamed(context, '/question'); inside the initState of the same screen | Each navigation pushes the same route again, creating an endless stack that the user never escapes. |
| Data‑binding loops | Two‑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
- User complaints – Users report “the app freezes on the first question” or “it keeps loading the same question”. Reviews on Google Play and the App Store often mention “stuck in a loop”, leading to a 2–5 ★ rating drop.
- Support cost – Each loop generates a support ticket. For a mid‑size quiz publisher, 200 tickets per month can translate to $4 k–$6 k in engineering time.
- Revenue loss – Monetization in quiz apps is usually ad‑based or via in‑app purchases (hints, extra lives). If users never reach the payoff screen, eCPM drops and conversion funnels break, cutting daily revenue by up to 30 % during the bug window.
- App store penalties – Repeated crash‑free but “unresponsive” reports trigger Google Play’s “Poor performance” warning and can lead to temporary removal.
---
3. Concrete Manifestations
| # | Symptom | Underlying Loop Type |
|---|---|---|
| 1 | The same question flashes repeatedly after tapping Submit. | State‑machine bug – answer handler re‑loads the current index instead of index+1. |
| 2 | Loading spinner never disappears on the Results screen. | Timer‑driven loop that never clears because quizFinished never becomes true. |
| 3 | App freezes for 10–30 seconds on launch, then crashes with “Stack overflow”. | Recursive setState inside build() that keeps calling loadNextQuestion(). |
| 4 | After 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. |
| 5 | Navigation back button returns to the same question screen instead of the previous menu. | Navigation stack misuse – push inside initState. |
| 6 | Accessibility screen reader repeats the same question text endlessly. | Data‑binding loop between UI label and live‑region model. |
| 7 | In “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
- Automated UI exploration (SUSA)
- Upload the APK or web URL to SUSA. The platform runs a curious persona that clicks every tappable element and records flow verdicts.
- SUSA’s flow tracking will flag a step that never reaches a PASS state (e.g.,
question → submit → same question). The generated Appium script will contain a loop‑detected annotation.
- Performance profiling
- Android Studio’s CPU Profiler or Chrome DevTools’ Performance tab show a long‑running call stack with repeated function entries (
loadNextQuestion → setState → loadNextQuestion).
- Log analysis
- Look for repeating log lines (e.g.,
Fetching question…appearing > 10 times within a second). - Enable SUSA CLI (
susatest-agent) in CI to capture JUnit XML with a customsection that records loop warnings.
- Watchdog timers
- Insert a watchdog in test builds: if a screen stays longer than a threshold (e.g., 5 s) without a state change, log INFINITE_LOOP_DETECTED.
- Static analysis
- Use detekt (Kotlin) or SonarQube rules that flag
while (!condition)loops without a break statement inside an async context.
- Accessibility testing
- Run WCAG 2.1 AA checks with SUSA’s accessibility persona. An endless live‑region update will be reported as an “ARIA live region not terminating”.
---
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
- Integrate SUSA into CI/CD
- Add a GitHub Actions step that runs
susatest-agentagainst the newly built APK. - Fail the pipeline if any flow verdict stays in *Running* for longer than a configurable timeout (default 8 s).
- Unit‑test the state machine
- Write exhaustive tests for every transition (
givenAnswerWhenCorrectThenAdvance,givenLastQuestionWhenSubmitThenResult). - Use property‑based testing (e.g., Kotlin
kotestcheckAll) to generate random answer sequences and assert the index never exceedsquestions.size.
- Static‑analysis rule set
- Enforce “no UI updates in render loops” and “no recursive calls without base case”.
- Add custom linters that detect
while (!condition)without anawaitordelayinside.
- Watchdog in production builds
- Ship a lightweight watchdog that logs a warning if any screen stays longer than a threshold.
- Pair with SUSA’s CLI to automatically upload these logs for post‑release analysis.
- Persona‑driven exploratory testing
- Use SUSA’s impatient persona to hammer the “Next” button repeatedly; the platform will surface any UI that never progresses.
- The elderly persona slows down interactions, exposing loops that rely on rapid clicks to break.
- Accessibility regression
- Run SUSA’s WCAG 2.1 AA suite on every PR. Infinite live‑region updates will be flagged as violations, preventing the loop from slipping into production.
- Security‑oriented checks
- OWASP Top 10 scanning (included in SUSA) can reveal infinite‑loop‑like DoS patterns where an attacker triggers repeated API calls.
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