Skip to Content

Projects

Loading...

Loading Service

Diagnosed and resolved a production bug where infinite loading screens and simultaneous overlay conflicts were degrading the user experience. Designed a reference-counted singleton with a withLoading(fn) wrapper that guarantees start/stop pairing via finally, then retrofitted it across async actions app-wide.

  • JavaScript
  • Svelte
  • SvelteKit
  • Singleton Pattern

Interactive Demo

Run both tasks concurrently and watch how each panel handles the loading overlay.

Without LoadingService

Broken
Event log
No events yet…

Task A finishes at 1s and sets isLoading = false — the overlay disappears even though Task B is still running.

With LoadingService

Fixed
Active tasks: 0
Event log
No events yet…

The overlay stays visible until both tasks complete — the counter only reaches 0 when all callers have called stop().

The Service

let loadingCount = 0;

function start() {
  loadingCount = loadingCount + 1;
  if (loadingCount === 1) setGlobalLoading(true);
}

function stop(forceStop = false) {
  if (forceStop) loadingCount = 0;
  if (loadingCount > 0) loadingCount = loadingCount - 1;
  if (loadingCount === 0) setGlobalLoading(false);
}

async function withLoading(fn) {
  start();
  try {
    return await fn();
  } finally {
    stop(); // always runs — even if fn() throws
  }
}

export const loadingService = {
  stop, // Note: We don't export start so that the service can't be started without a withLoading call.
  withLoading,
  get running() {
    return loadingCount > 0;
  },
};

The reference counter is the key insight: start() increments and stop() decrements, so the overlay only clears when every caller has finished — not just the first one that returns. Wrapping calls in withLoading() makes cleanup automatic: the finally block guarantees stop() runs even if the wrapped function throws an exception, preventing the overlay from getting permanently stuck.

Usage

// Before: each action manages its own loading flag
async function saveUser(data) {
  isLoading = true;          // flickers off between calls
  await api.save(data);
  isLoading = false;
}

// After: withLoading handles start/stop automatically
async function saveUser(data) {
  await loadingService.withLoading(async () => {
    await api.save(data);
  });
}