Before this lesson
Complete Fetch and promises. Understand debouncing and loading states.
What you will be able to do
Protect the UI with request identity and distinguish loading, success, empty, failure and cancellation.
Understand the mechanism
An earlier query can finish after a later query and overwrite the desired result. Debounce reduces request frequency but does not fix already-started work finishing out of order. Define ownership of state updates first.
Assign an increasing ID and allow updates only from the latest request. Apply that rule to success, catch and finally; an old finally can clear a newer loading state even when stale data is ignored.
Cancellation saves resources where supported but cannot cancel every asynchronous operation or roll back server work. Request identity protects consistency, while cancellation manages resources. Treat intentional abortion differently from genuine failure.
The example uses controlled delays to reproduce ordering without external services. A real fetch adds HTTP, parsing and schema checks. Deliberately slowing the old request provides stronger evidence than repeatedly clicking under normal network conditions.
Runnable example
Run HTML examples as separate .html files in a modern browser. Prepare resource files and multi-file modules as described in the lesson.
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Honeysuck practice</title></head><body>
<button id="old">Slow HTML request</button><button id="new">Fast CSS request</button><button id="fail">Failing request</button><p id="status" role="status">Ready</p><pre id="result"></pre>
<script>
let latest=0;
const status=document.querySelector('#status'),result=document.querySelector('#result');
function service(label,delay,fail=false){return new Promise((resolve,reject)=>setTimeout(()=>fail?reject(new Error('Demo failure')):resolve([label]),delay));}
async function search(label,delay,fail=false){const id=++latest;status.textContent='Loading '+label;result.textContent='';
try{const items=await service(label,delay,fail);if(id!==latest)return;result.textContent=items.join(', ');status.textContent=items.length?'Loaded':'No matches';}
catch(error){if(id!==latest)return;status.textContent='Could not load. Try again.';}
finally{if(id===latest)document.body.dataset.busy='false';}
}
for(const [selector,label,delay,fail] of [['#old','HTML',700,false],['#new','CSS',100,false],['#fail','Failure',150,true]])document.querySelector(selector).addEventListener('click',()=>{document.body.dataset.busy='true';search(label,delay,fail);});
</script>
</body></html>
Trace and verify
Click Slow then Fast; CSS remains after the slow delay expires. An old failing request cannot overwrite a newer success. A standalone failure displays an error and a subsequent success recovers. Remove guards temporarily to prove the test reproduces the defect, then restore them.
Common mistakes and boundaries
The busy data attribute is only demo state; apply appropriate aria-busy and text feedback in a real interface. Never declare success unconditionally in finally.
Practice challenge
Add an empty-result request and synchronize aria-busy with the current request only.
Reference solution and reasoning
Set busy at start and clear only for the latest completion. Test a slow old query followed by an empty new query; the empty state must persist.