Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

One Global Spinner for Every AJAX Call

C
Carlo Fontanos
· 2 min read

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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading