One Global Spinner for Every AJAX Call
Legacy admin panel, forty-something AJAX calls, and the ticket says "add a loading indicator". The junior approach touches all forty call sites. The jQuery-fluent approach is eight lines, once:
$(document)
.ajaxStart(() => $('#global-spinner').fadeIn(150))
.ajaxStop(() => $('#global-spinner').fadeOut(150));
jQuery fires ajaxStart when a request begins and no other request is running, and ajaxStop when the last concurrent request finishes. That concurrency bookkeeping - the annoying part of a global spinner, where three overlapping requests must not flicker the indicator - is already done for you.
The full lifecycle family
$(document).ajaxError((event, xhr, settings) => {
if (xhr.status === 401) location.href = '/login'; // session expired anywhere
else if (xhr.status >= 500) toast('Server error. Try again.');
console.warn('AJAX failed:', settings.url, xhr.status);
});
$(document).ajaxSend((e, xhr, settings) => {
console.debug('→', settings.type, settings.url); // poor man's request logger
});
ajaxError as a centralized session-expiry handler is the sleeper hit: no more "why does the page pretend to work after logout" bugs, because every 401 from anywhere redirects. Per-request .fail() handlers still run for request-specific logic - global and local coexist.
$.ajaxSetup: inject headers everywhere
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
'X-Requested-With': 'XMLHttpRequest',
},
timeout: 15000,
});
Every $.ajax, $.post and $.get now carries the CSRF token - the exact pattern Laravel's docs recommend - plus a default timeout, which is the setting everyone forgets until a hung request freezes a workflow forever. (Word of caution: don't put success/error callbacks in ajaxSetup; shared callbacks create spooky action at a distance. Headers and timeouts: yes. Behavior: no.)
Two footnotes
- Requests fired with plain fetch() or XMLHttpRequest bypass all of this - it's jQuery-internal, not a network tap. Mixed codebases need the interceptor pattern below for the fetch half.
- Turn off global events per request with
global: false- right for background polling that shouldn't flash the spinner every ten seconds.
The fetch-world equivalent (since you'll ask)
const _fetch = window.fetch;
let active = 0;
window.fetch = async (...args) => {
if (++active === 1) spinner.show();
try { return await _fetch(...args); }
finally { if (--active === 0) spinner.hide(); }
};
Same idea, hand-rolled counter. jQuery's version has been doing this since 2006 - one of many cases where the old library's ergonomics still embarrass the modern platform. For the requests themselves, pair this with delegated events and half your legacy AJAX bugs disappear.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.