Learning Objectives
- Say what the word Ajax named in 2005, what it did not invent, and why giving a pattern a name is what made it spread
- Walk a scripted request end to end without a navigation — trigger, target, object,
open,send, state callback, response, DOM — and say what is happening at each movement - Explain why the X is the least accurate letter in the name, and what almost everyone sends instead
- Enumerate what the browser stops doing for you the moment the page issues its own request: in-flight feedback, failure reporting, the URL, session history, cancellation, the announcement to assistive technology, and working at all with scripting off
- Judge Ajax per interaction rather than per application, and name what it buys when the answer is yes
A name for something people were already doing
On 18 February 2005, Jesse James Garrett published an essay at Adaptive Path called Ajax: A New Approach to Web Applications. It argued that a handful of technologies already present in every browser added up to an approach: the DOM, JavaScript, CSS, and an object for making HTTP requests from script. It gave that approach a word. He spelled it Ajax, as a name rather than an initialism, which turned out to be the right instinct.
Nothing in the essay was new. The interesting thing about this date is not an invention.
The lineage runs back most of a decade. Internet Explorer introduced the <iframe> in 1996, and people immediately started hiding one, pointing it at a script-generated page, and reading the result. The same trick worked with a hidden frame in a frameset. Microsoft shipped a feature called remote scripting in 1998, and libraries with names like JSRS (JavaScript Remote Scripting) circulated for years. Then the Outlook Web Access team built the object that mattered: XMLHTTP, shipped as an ActiveX control in MSXML 2 with Internet Explorer 5.0 in March 1999. Mozilla reimplemented it as a native JavaScript object under the name XMLHttpRequest, Safari and Opera followed, and Internet Explorer 7 finally exposed it natively in 2006. By the time the essay appeared, the technique was nearly six years old and had already shipped in a mass-market Microsoft product.
What 2005 added was a noun. That sounds like the least significant possible contribution and it was not. Before the word, this was a collection of tricks with no collective identity: you could do it, but you could not put it in a job posting, title a book with it, propose a conference talk about it, or argue with a colleague about whether it belonged in a project. "Should this screen use Ajax?" is a question a team can actually have. "Should this screen use the hidden-iframe thing, or the ActiveX control, or the one Mozilla added?" is not a question. It is a shrug.
The other half of the mechanism is chronology, and it is easy to get backwards. The essay did not set off a wave of applications. The applications came first, and the essay is about them. Gmail shipped in April 2004. Google Suggest shipped in December 2004. Google Maps shipped on 8 February 2005, ten days before the essay, which names all three as its evidence. By the time the word existed, a great many people had already dragged a map that did not reload and watched a search box answer them as they typed. What they did not have was a term for it. Garrett did not announce a technique. He named one everyone had already seen working. That is why the name traveled as fast as it did.
What did not travel intact was the acronym.
The X was never very true
Ajax was shorthand for Asynchronous JavaScript and XML. Two of those three have held up.
- Asynchronous is the word that matters, and it is still accurate. The request is issued, the script returns, and the page keeps running. Everything difficult about this step follows from that one property.
- JavaScript is accurate and uninteresting.
- XML is the one that did not survive contact with practice. Almost nobody sends XML. JSON took over so completely that most working developers have never parsed an XML response on purpose: no schema language, no namespaces, no parser to install, and a one-to-one correspondence with the data structures JavaScript already had.
The object's own name is the fossil left behind. XMLHttpRequest will still parse an XML response into a document for you and hand it back on responseXML, and approximately nobody uses that property. In 2005 the JSON half was not free either: JSON.parse did not become a built-in until ES5 in 2009, so period code either loaded Douglas Crockford's json.js or, far too often, ran the response body through eval. That is as bad an idea as it sounds. Step 07 returns to why.
What is not out of date here
The acronym aged badly; the object did not. XMLHttpRequest is not deprecated, is not scheduled for removal, is specified as a living standard, and ships in every browser you will meet. It is also not merely a legacy path: it can report upload progress, which fetch still has no portable equivalent for, and step 04 is largely about the parts of it you will still use.
One real deprecation, since it comes up: passing false as the third argument to open makes the request synchronous, which blocks the main thread until the server answers. That has been formally deprecated for years and current browsers log a console warning about it. It still exists and still runs, so you will meet it in old code. Do not write it. It makes a scripted request worse for the user than the full-page reload it replaced.
Hello Ajax World, step by step
Here is the whole thing, in eight movements. Read it as choreography rather than as a program: the order the lines appear in the file is not the order they run in, and getting comfortable with that gap is most of what this step is teaching.
1. The trigger
Something has to start it. In the postback version this was a form that submitted; here it is the same form, intercepted. The preventDefault call is the single line that stops step 02 from happening.
Keeping the <form> rather than reaching for a bare button is deliberate. The form still gives you the label associations, the Enter key, and the submit semantics. You are borrowing its behavior and declining one specific part of it.
What it does not give you for free is a working page when the script never loads. Keeping the element leaves you the shape of a fallback, the markup a browser already knows how to submit. But this form has no action and no method, so with scripting off it submits nowhere and the control does nothing. Turning that shape into a fallback means giving the form a real action and having a server ready to answer it. That is work, and it is the last item on the bill below.
2. The target zone
A navigation does not need a target, because the target is the entire document. The moment you issue the request yourself you have to name the region you intend to replace. You are now responsible for everything inside it.
Note what that markup implies: there is content in there already. The list arrives with the page, so it is readable before a line of script runs and readable if the script never runs. That is a choice. It is the difference between enhancing a page and requiring JavaScript to see anything at all.
3. Creating the object
One line today. In 2005 it was five, because Internet Explorer 5 and 6 had no such global and required an ActiveX control instead. Every Ajax tutorial of the era opened with some version of this, and a surprising amount of it is still sitting in codebases:
That block is a browser-support question that was live and expensive in 2005 and is gone now. If you find it in a file you are maintaining, delete it. It carries no risk.
4. Opening the request
Three arguments: the method, the URL, and whether the request is asynchronous. Despite the name, open does not open anything on the network. It configures the object; nothing is sent. This is also where the query string gets built. Note that encodeURIComponent is doing a job the form in step 01 did for free.
The third argument defaults to true and should be left that way. Passing false is the synchronous mode described above: the browser stops running your page until the answer arrives.
5. Sending
This is where the request goes out. What happens next is nothing. It returns immediately, usually before the server has heard of the request. The next statement runs while the request is still in the air. Any code that assumes the answer is available on the following line is wrong. Later steps reach for promises and await because this is hard to keep straight by hand.
A GET sends no body, so the call takes no argument. A POST body goes here, which is step 06's subject.
6. The state callback
Since the answer does not arrive on the next line, you leave instructions for when it does.
The handler is assigned before send and runs after it, potentially several times, as the request moves through its states. Nearly all real code checks for 4 and ignores the rest. That is defensible: state 2 only tells you the headers landed, and state 3 fires repeatedly while the body streams in, which is useful for a progress bar over a large download and for nothing else most days.
Two footnotes that step 04 makes into a whole section. First, this is not the modern way even within XMLHttpRequest: it has had ordinary load, error, and timeout events for well over a decade, and they are better in every respect. Second, readyState === 4 means finished, not succeeded. A 500, a 404, a failed connection, and a request you canceled yourself all arrive here.
7. Reading the response
responseText is a string: the raw body, as it came off the wire. JSON.parse turns it into data. The status check separates "the exchange completed" from "the exchange completed and the server said yes." Those are not the same fact.
One value deserves naming now because it confuses everyone once: status === 0. That is not a status code the server sent; it means no response line was ever received. A request you aborted, a connection that failed, and a cross-origin request the browser refused to let you read all present as 0. Step 07 and step 08 are about telling those apart.
8. Putting it in the page
This movement has no counterpart in the postback version, because there the browser did it. Everything about it is now a decision you are making: which element, replaced or appended, in what order, and whether the values coming back from the server are inserted as text or as markup. That last one bites. textContent treats the response as the untrusted input it is. innerHTML treats it as code you wrote. Step 07 spends real time on the difference. Use textContent now.
Read the eight movements in time order rather than file order. Movements 1 and 2 are in place before anything happens. When the user acts, movements 3, 4, 6, and 5 all run in a single uninterrupted burst: create the object, configure it, attach the callback, send. The only ordering rule that matters is that the callback is attached before the send. Then nothing happens at all, for anywhere between ten milliseconds and forever. Movements 7 and 8 run at the end of that gap. That gap, where the page is live and the answer is not here yet, is the subject of the next section.
Demo: the first version that does not navigate
The page below is the step 02 demo with one thing changed: the form is intercepted, and the eight movements above run instead of a submission. The instrumentation strip is in the same place and reports the same measurements as step 02's, plus two the postback version had no use for. Requests this page has made only counts above zero once a script is issuing them. Query string in the address bar only becomes interesting once it has stopped changing. One difference to note before you start: this demo opens showing all eight posts, where step 02's opened showing three.
Run the same experiment in the same order. One: type something into Notes (not submitted). Two: scroll all the way down to the bottom marker. Three: change the limit and press Apply.
Nothing moved. You are still at the bottom of the page, the notes field still has your text in it, and the list below the control has different rows in it than it did a second ago. In the strip at the top, Document loads this session has not budged. It reads 1 on a first visit, and whatever it reads when you arrive is what it will read when you leave. This document was built at still shows, to the millisecond, the moment you got here. Same document, still running, still holding everything it was holding. Press Apply four more times and both of those stay where they are while Requests this page has made climbs.
One field in that strip is there to spoil the mood, and you should look at it now rather than in the next section: Query string in the address bar. It says (none). It said (none) when you arrived and it will still say (none) after you have changed the list five times. In step 02 the query string was the application's memory. The ?limit=5 in the address bar was the only record of what you had asked for, and the only thing the next document had to go on. Here it has stopped describing the page at all.
The waterfall fills up — and the reason is not that the tool improved
Steps 01 and 02 both embedded <http-waterfall capture="true"> and both sat there showing No requests to display no matter what you did to the demo itself. The only way to get a row into either was the panel's own Request Builder, which sends from the document rather than from the feature. Both times the callout promised this step would be different. Press Apply and it is: the GET /api/posts?limit=… is listed, with its status, its timing, and its response body.
The component is byte-for-byte the same component. It captures by replacing fetch and XMLHttpRequest in the document with wrappers that report what goes through them, which is all it has ever done. What changed is on the other side: a form submission and a page navigation are issued by the browser itself, where no script can reach them, and an XMLHttpRequest is constructed by the page's own code from a global the page looks up by name.
The panel did not get better. The request moved inside the page's reach. You can see these requests now because you are the one making them. The same move hands you their failures.
Try one experiment while it is open: press Apply three times in quick succession, then read the top of the panel. It keeps everything it has captured, so there will be more rows than that in there. It lists them newest-completed-first, so the three you just made are at the top. Two of those three carry a status of 0 rather than 200. Those two are the requests the demo canceled because a newer one had already started. The one that survived is the newest and sits above them, because it is also the last to finish. That is the abort call described below, showing its work.
What you just took on
The demo above is about two dozen lines of request handling, and for one interaction on one page it is fine. The trouble is that nobody stops at one. The second interaction is another fifteen lines shaped almost the same way; by the tenth, the page has ten hand-rolled request paths, ten ways of showing that something is loading, and ten opinions about what the screen should say when the server is down. There is no architecture in that code, because nothing about the technique suggested one.
That is the road the industry actually walked down between roughly 2005 and 2010, and the destination was the client-side framework. If you have ever wondered why every framework of the last fifteen years arrives with opinions about models, views, state, and routing baked in before you have written a line: this is why. Those are the pieces that fell on the floor when the page took over the request. A framework is an offer to pick them up for you.
The critics said so at the time, and they were largely right. Ajax obviously worked. The objection was that it was being sold as a small change to a page when it was a large change to an architecture, and that the bill would arrive later and be paid by users. It did, and it was. Here is the bill, itemized. Every line is something the browser was doing for the page in step 02 without being asked.
- What shows while it is in flight. A navigation has visible browser machinery behind it. A scripted request has none: the page looks exactly as it did a moment ago, whether the answer is thirty milliseconds away or never coming. If the user is to know that anything is happening, you have to say so.
- What happens when it fails. The browser has an error page for a failed navigation, and users recognize it. There is no error page for a failed
XMLHttpRequest. There isstatus === 0, and whatever you decide to do with it. - Whether the URL still describes the page. This is the one the demo makes impossible to ignore. The list changed five times and the address bar never moved, which means the view on screen cannot be bookmarked, cannot be sent to anyone, cannot be opened in a second tab, and does not survive a refresh.
- Whether the back button does anything sane. There is no session history entry, because no navigation happened. Back does not undo the last change; back leaves the page.
- Whether anything can cancel. A user can press a control faster than a server can answer. Two requests are then in flight, and nothing guarantees they finish in the order they started. Without care the screen ends up showing the older answer. The demo keeps a reference to the request in flight and aborts it before starting another, which is the smallest possible version of a problem step 07 takes seriously.
- Whether a screen reader is told anything changed. A navigation announces a new document. Replacing the contents of a
<ul>announces nothing at all: focus stays where it was, no document loaded, and as far as assistive technology is concerned the page is exactly as it was. The demo pays for the cheapest corner of this: onerole="status"on the line under the button, so a short report of the result is spoken. Note what that is and is not: it announces the message in that one line, not the fact that the list mutated. One attribute is more than a great deal of shipped software manages. It is still not the same as doing this properly. - Whether it works at all with scripting off. Step 01's form and step 02's postback both did their whole job without a line of JavaScript, because the browser was submitting the form and the server was answering it. The demo above cannot: its
<form>has noactionand nomethod, so if the script fails to load, throws on the way in, or is turned off, pressing Apply does nothing at all. Its own<noscript>says so. Note how narrow the loss is and is not: the eight posts are still readable, because they came with the document. The control is gone. Getting it back means giving the form a realactionpointing at a URL a server will answer, and then intercepting that form rather than inventing one. That is the whole of progressive enhancement, and a second code path to build and keep working.
Two of those had no first-class solution in 2005 and do now. That difference matters: the 2005 limits were not a permanent state of affairs. There was no history.pushState then, so keeping the URL honest meant abusing fragment identifiers: the part of the URL after the #, which the browser records in history but never sends to the server. Google published an entire "Ajax crawling scheme" in 2009, the source of that era's #! URLs, so that such pages could be indexed at all, then deprecated it in October 2015 once its crawler could render JavaScript. Today the History API is universal and the major search engines run your script. The platform has handed back the tools for two items on that list. It has not handed back the behavior, and cannot. Every line above is still code somebody has to write, remember to write, and get right.
The sentence to take away from this step
The postback solved these problems. Ajax hands them back to you.
History, addressability, the back button, failure reporting, and "something is happening" arrived in step 02 as a single package with no configuration and no bugs, because they were the browser's implementation rather than yours. Issuing the request from the page does not remove those problems from the application. It removes the implementation and leaves the problems, individually, on your desk. Every remaining step in this series is one of them coming back.
The conclusion is not "do not do this," which would be a strange thing to argue on a page you are reading in a browser that would be unusable without it. It is narrower: you do not have to take the whole bundle apart to fix one interaction. A page can be an ordinary document-shaped page, with real URLs and a working back button, and still update one list in place when a filter changes. The mistake of the late 2000s was not using Ajax. It was concluding that because one interaction wanted it, the whole application should be rebuilt around it. Then came a decade of rediscovering why the browser had been doing all of that.
What is actually better
A step that ended on the section above would be dishonest by omission. Ajax won for reasons, the reasons are still good, and three of them are measurable rather than aesthetic.
- What crosses the wire is the answer, not a building. Step 02's arithmetic was tens of kilobytes of HTML to change a handful of list items. The same change here is a few hundred bytes of JSON. Open DevTools on the demo, press Apply, and read the transfer size next to
/api/posts; then compare it with the size of the document itself. The ratio is the whole argument, and you can measure it. When this was written the demo document was about 23 KB and the JSON for three posts was 448 bytes. Both of those numbers compress on the way to you, so the column to read is the transfer size rather than either raw figure. - Nothing is torn down and rebuilt. No document is discarded, no DOM is reparsed, no scripts are re-executed, no global environment is thrown away and constructed again. The saving is not only network time; a large page costs real work to build, and this skips all of it.
- State survives by default rather than by effort. Scroll position, focus, selection, half-typed fields, expanded rows, anything the page had computed: all of it is still there, because the page is still there. Step 02's demo needed code to write two numbers to
sessionStoragejust to describe what it had lost. This one needs no code at all to keep it.
And then the category that is not an improvement on the postback but an escape from it: interactions that cannot exist as a navigation at all. Type-ahead suggestions that update while you are still typing. A draft that saves itself every few seconds without interrupting you. A username field that says "taken" before you reach the next field. Infinite scroll, live validation against a server, a dashboard that keeps its own numbers current. None of these is a faster version of a round trip. None has a page-per-URL equivalent, because they all require the page to still be here afterward.
So here is the rule that decides this in practice. The judgment is per interaction, not per application. The question is never "is this an Ajax app?" That framing is how the last section's mistakes got made. Ask instead: for this control, on this screen, does the result deserve its own URL that someone might share or return to, and does the user do it often enough that a round trip is in the way?
A search that produces a results page: give it a URL, let it be a navigation. A checkbox that reorders that page's results, which the same user will tick and untick eleven times in a minute: make the request from the page. Both of those can be true on the same screen. The best-behaved applications on the web made that call one control at a time.
Next Steps
Step 03 used XMLHttpRequest the way 2005 used it. In Step 4: XHR up close, we stop using it and start reading it:
openandsendin full: every argument, what is legal before and after each call, and what actually goes on the wirereadyStateand the event model that replaced it:load,error,abort,timeout, andprogress, and whyonreadystatechangesurvives only in old code- Status,
statusText, and reading response headers, including the ones you are not allowed to see, and why - Setting request headers yourself, and the list the browser will not let you set
abortandtimeoutas a first look at controlling a request you have already sent- Why any of this still matters when
fetchexists: it is in code you will be asked to maintain, and it still reports upload progress, whichfetchhas no portable equivalent for