AJAX isn't a separate language learned from scratch it's a technique for coordinating JavaScript, the browser's request mechanism, and a server response to update part of a web page without reloading the whole thing. That coordination is where assignments get genuinely difficult, because the moment you introduce a request that takes time to complete, your code has to account for the gap between "the request was sent" and "the response arrived." Most AJAX bugs come directly from code written as though that gap doesn't exist.
Code Written as if the Response Arrives Instantly
This is the single most common structural error in AJAX assignments, and it's not a typo it's a misunderstanding of how asynchronous execution works. A student sends a request, then immediately writes logic on the next line that uses the response data, before the server has replied. On a fast local development server, this sometimes appears to work the response arrives so quickly that the timing issue isn't exposed. Against a slower network, a marker's different setup, or a server under any kind of load, it fails consistently because the data genuinely isn't there yet when the code tries to use it.
With the XMLHttpRequest approach, the pattern shows up as logic placed after xhr.send() rather than inside the onreadystatechange or onload callback. With the Fetch API, it appears as code written after a fetch() call but before the awaited .then() chain resolves or, in async/await code, as a missing await keyword before a fetch() or response.json() call. The fix in both cases is the same: everything that depends on the response data must live inside the callback or after the awaited promise resolves, not written in sequence and assumed to run in order. Our AJAX implementations structure this correctly from the first line written, not as a patch applied after testing reveals the problem.
The Fetch API Doesn't Behave the Way Students Assume on HTTP Errors
One of the most important differences between XMLHttpRequest and the Fetch API is how each handles HTTP error responses. With XHR, checking xhr.status for anything outside 200–299 is the standard pattern. With Fetch, the returned Promise resolves (rather than rejects) even when the server returns a 404 or 500 because from Fetch's perspective, it successfully received a response. The response's ok property is false, and response.status carries the error code, but the .catch() handler is not triggered. Students who write fetch(url).then(handleSuccess).catch(handleError) and assume the catch will fire for a 404 are writing code that silently ignores server errors and passes the failed response body into the success handler. The correct pattern checks response.ok explicitly: if (!response.ok) throw new Error(response.status) before parsing the JSON. This is one of the most commonly noted errors in AJAX marking feedback, and it's one that only manifests when an endpoint actually returns an error which is exactly what a marker testing edge cases will cause to happen.
DOM Updates That Target the Wrong Element or Discard Event Listeners
Once a response arrives, updating the page correctly requires selecting the precise element to modify and changing only what needs to change. A common mistake is overwriting an entire container's innerHTML on every request rather than updating a specific child element. Overwriting innerHTML destroys all event listeners attached to child elements by JavaScript any click handlers, input handlers, or custom event listeners registered on those children are removed, because the elements they were attached to no longer exist as the same DOM nodes. The replacement elements are newly created from the HTML string and have no listeners. This produces an application that works correctly after the first request, loses its interactivity after the second, and becomes completely unresponsive after the third. The correct approaches are targeted DOM manipulation with element.textContent, element.setAttribute(), or element.style for element level updates, or DocumentFragment construction and targeted appending for list updates that need to add new elements without destroying existing ones. Loading state management showing a spinner or disabling the triggering button while a request is in progress prevents duplicate requests from rapid successive clicks and provides user feedback during network latency.
JSON Parsing Errors and Missing Structure Handling
JSON parsing errors in AJAX code tend to fail in ways that are difficult to trace. response.json() in the Fetch API returns a Promise that rejects if the response body is not valid JSON. If this rejection is not caught because the developer only chained a .catch() at the outer level that handles network failures but not parsing failures the error propagates silently and stops all subsequent code from running. The same problem occurs when the server returns valid JSON but in a structure the code doesn't expect: the code assumes an array and receives an object with an array nested inside a property, accesses data[0] expecting the first item but gets undefined because data is {items: [...]} rather than [...]. Defensive access checking that the expected property exists before using it, using optional chaining (data?.items?.[0]), and handling the null/undefined case explicitly prevents these silent failures from reaching the DOM update code. Our AJAX implementations validate the response structure before attempting to use it, and handle all JSON parsing failure paths explicitly.
Testing Only Against Fast Local Servers
An AJAX assignment tested exclusively against a local development server with an effectively instant response time can hide all the timing problems described above. The same code, tested against a server with 200ms response latency which is a realistic figure for any non trivially distant API endpoint reveals every race condition and every piece of DOM manipulation that assumed the data was already present. We test every AJAX implementation against deliberately varied response timing and simulated failures before delivery, not just the fastest case local scenario. CORS configuration is also verified where the assignment connects to an external API a request that works in development against a same origin server may fail against a cross origin API that requires correct request headers, and this failure only becomes visible when the endpoint actually enforces the policy.