jQuery / .ajax() / Synchronous Requests and an Asynchronous Alternative

Setting async: false in jQuery.ajax() makes a synchronous request: the current execution flow waits until it finishes. This can block the browser’s main execution and responsiveness, so prefer default asynchronous requests and connect subsequent work. A second request that needs the first response can still run in order without blocking.

What async:false means

The jQuery.ajax() API defaults async to true. false does not make a request faster; it prevents code after the call from continuing before completion. MDN’s synchronous and asynchronous request guide explains the resulting unresponsive interface.

When inspecting legacy synchronous code, use the success, error and complete options. jQuery has deprecated async:false combined with jqXHR’s Deferred handling since 1.8. This does not deprecate all Ajax or asynchronous .done(). The .then(), .done() and .fail() below belong to the asynchronous alternative.

If ordering is the real requirement, start the next request after receiving the first response and return that request to connect it. Distinguish independent parallel requests from requests requiring a previous response value.

A delayed local server and complete client

Prepare Python 3, Internet and a new practice folder. Save ajax-server.py as UTF-8. It serves files and three APIs from one origin. /first returns id 7; /second uses its parent parameter; /slow intentionally waits longer for the timeout exercise. Delays are deliberate server conditions.

from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import json
import sys
import time

class Handler(SimpleHTTPRequestHandler):
    def do_GET(self):
        url = urlparse(self.path)
        if url.path not in ("/first", "/second", "/slow"):
            return super().do_GET()
        time.sleep(2 if url.path == "/slow" else 0.4)
        if url.path == "/second":
            parent = parse_qs(url.query).get("parent", [""])[0]
            data = {"detail": "Details for " + parent}
        else:
            data = {"id": 7, "name": "Alpha"}
        body = json.dumps(data).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        try:
            self.wfile.write(body)
        except (BrokenPipeError, ConnectionResetError):
            pass

if __name__ == "__main__":
    port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
    ThreadingHTTPServer(("127.0.0.1", port), Handler).serve_forever()

Save ajax-demo.html in the same folder. Use the official full jQuery file 4.0.0, not slim without Ajax. Start controls are disabled while busy. active stores the actual current jqXHR. Returning the second request from .then() makes the following success/failure chain wait for it too.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Keep request order without blocking</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 24px auto; padding: 16px; }
    button { font: inherit; margin: 4px; max-width: 100%; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <h1>Sequence asynchronous requests</h1>
  <button type="button" class="start" id="run">Run two requests</button>
  <button type="button" class="start" id="error">First request fails</button>
  <button type="button" class="start" id="timeout">First request times out</button>
  <button type="button" id="cancel" disabled>Cancel active request</button>
  <button type="button" id="reset">Reset</button>
  <p id="heartbeat">heartbeat=0</p>
  <pre id="result" aria-live="polite">Ready</pre>
  <script>
    $(function () {
      let active = null, busy = false, lines = [], ticks = 0;
      setInterval(function () { ticks++; $("#heartbeat").text(`heartbeat=${ticks}`); }, 100);
      function log(line) { lines.push(line); $("#result").text(lines.join("\n")); }
      function run(path, timeout) {
        if (busy) return;
        busy = true; lines = [];
        $(".start, #reset").prop("disabled", true); $("#cancel").prop("disabled", false);
        log("before");
        active = $.ajax({ url: path, dataType: "json", timeout: timeout });
        active.then(function (first) {
          log("first=" + first.name);
          active = $.ajax({ url: "/second", data: { parent: first.id }, dataType: "json", timeout: 1000 });
          return active;
        }).done(function (second) {
          log("second=" + second.detail);
        }).fail(function (xhr, textStatus) {
          log("failed=" + textStatus + "; HTTP=" + xhr.status);
        }).always(function () {
          log("complete"); active = null; busy = false;
          $(".start, #reset").prop("disabled", false); $("#cancel").prop("disabled", true);
        });
        log("returned to caller");
      }
      $("#run").on("click", function () { run("/first", 1000); });
      $("#error").on("click", function () { run("/missing.json", 1000); });
      $("#timeout").on("click", function () { run("/slow", 300); });
      $("#cancel").on("click", function () { if (active) active.abort(); });
      $("#reset").on("click", function () { lines = []; $("#result").text("Ready"); });
    });
  </script>
</body>
</html>

Initial output follows. A separate timer increases heartbeat every 100ms.

Ready

Open a terminal in the practice folder and run this. If python is missing, check installation and paths. If 8000 is occupied, run python ajax-server.py 8001 and update the browser address.

python ajax-server.py

Open http://127.0.0.1:8000/ajax-demo.html while the server runs, rather than file://. Internet loads the jQuery content delivery network (CDN). Stop the server with Ctrl+C afterward. The example ran in Windows Edge with 3.7.1 and 4.0.0 and the Python server; only the verification port was changed to an available one.

Check ordering, failure and cancellation

  1. Run two requests first shows before and returned to caller. heartbeat continues while waiting.
  2. Then first=Alpha, second=Details for 7 and complete appear. Network shows /second?parent=7 after the first response. The requests do not start together.
  3. First request fails gives failed=error; HTTP=404 and complete. No second request starts.
  4. First request times out limits /slow’s two-second delay to 300ms and reports failed=timeout; HTTP=0. Zero is not an HTTP code returned by the server.
  5. Cancel active request aborts the current request and reports failed=abort; HTTP=0. After the first response, active points to the second request.
  6. Completion reenables start/reset controls. Reset clears only the trace to Ready; heartbeat continues. Verify a normal retry after failure.

.always() cleans buttons and busy state after success or failure. Success-only cleanup could leave controls unusable after a failure. Keep the returned chain distinct from the actual abortable jqXHR, updating active on each $.ajax(). Aborting a client request does not roll back work already performed by the server.

Inspect legacy synchronous order

With the local server running and other requests finished, run this once in the developer tools Console. It explains legacy async:false using this server’s /first only; do not replace it with a slow external address.

console.log("before synchronous request");
setTimeout(function () { console.log("timer after synchronous request"); }, 0);
$.ajax({
  url: "/first",
  dataType: "json",
  async: false,
  success: function (data) { console.log("success=" + data.name); },
  error: function (xhr, textStatus) { console.log("failed=" + textStatus); }
});
console.log("after synchronous request");
before synchronous request
success=Alpha
after synchronous request
timer after synchronous request

success precedes the line after the call; the zero-delay timer runs after the synchronous call and subsequent code. Zero milliseconds does not interrupt current execution. A browser may show a synchronous-request deprecation warning; hiding it does not solve blocking.

Writing Loading before a synchronous request may not give the browser enough opportunity to paint it or process timers and input. Compare heartbeat in the asynchronous client to distinguish waiting for a response from blocking the main flow.

What to change in existing code

Simply changing async to true in code that stores an external variable and returns immediately may read it before it exists. Move dependent work into completion or return a request/Promise for the caller to await. Creating a request in .then() without returning it does not make the chain wait.

Timeout differs from malformed responses, network failure and user cancellation. Real programs should validate response structure before using values. Adding timeout or cancellation options to synchronous requests does not make their main-thread blocking disappear. For submissions or saves, choose retry behavior according to the operation’s duplicate-execution consequences.