Skip to the solution
HardFrontend11 min read

How does defineAsyncComponent work with loading, error and retry in 2025?

795 views

The problem

Question presented to candidate: "Our analytics page lazy-loads a heavy chart with defineAsyncComponent. On flaky mobile networks the chunk sometimes fails and users see nothing. Walk me through the options defineAsyncComponent gives you for loading and error states, how you would add retries, and what changed recently."

What a strong answer should cover:

  • defineAsyncComponent(loader) returns a wrapper component that calls the loader the first time it renders and caches the resolved component for every later use.
  • The options object: loader, loadingComponent, errorComponent, delay (default 200 ms before the loading component shows), timeout (no timeout by default), suspensible, onError(error, retry, fail, attempts) and, since 3.5, hydrate.
  • Retry through onError: call retry() to run the loader again, or fail() to give up and show errorComponent.
  • Inside a <Suspense> the component is suspensible by default and its own loading, error, delay and timeout options are ignored; suspensible: false opts out.
  • Lazy hydration (3.5): hydrateOnIdle, hydrateOnVisible, hydrateOnMediaQuery, hydrateOnInteraction for server-rendered pages.
  • The loader must resolve to a component or an ES module whose default is one.

Clarifying questions expected:

  • "How many retries are acceptable before showing an error, and should the user be able to retry manually?"
  • "Is the page server-rendered?"

Code / implementation expected: Yes β€” a defineAsyncComponent with loading and error components, delay, timeout and an onError retry policy.

  • async-components
  • defineAsyncComponent
  • loading

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

11 min read Β· 11 sections

Read the code

VuedefineAsyncComponent with delay, timeout, loading and error components and an onError retry policy
<script setup>
import { ref, defineAsyncComponent, h } from "vue";

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let calls = 0;
const failFirst = ref(2); // how many loader calls fail before one succeeds

// Stands in for () => import("./Chart.vue"): the network fails twice, then works.
async function loadChart() {
  const n = ++calls;
  console.log(`  loader call #${n}`);
  await sleep(60);
  if (n <= failFirst.value) throw new Error(`chunk request ${n} failed`);
  // A real import() resolves to a module namespace and Vue unwraps .default;
  // a plain object is returned as the component itself.
  return { render: () => h("p", { class: "chart" }, "Chart loaded") };
}

const Chart = defineAsyncComponent({
  loader: loadChart,
  loadingComponent: { render: () => h("p", "Loading chart...") },
  errorComponent: { props: ["error"], render() { return h("p", { class: "err" }, `Error: ${this.error.message}`); } },
  delay: 40,       // show loadingComponent only if loading takes longer than 40ms
  timeout: 1000,   // give up (show errorComponent) after 1s
  onError(error, retry, fail, attempts) {
    console.log(`  onError attempt ${attempts}: ${error.message}`);
    if (attempts <= 3) retry(); // retry up to 3 times
    else fail();
  },
});

const show = ref(false);
</script>

<template>
  <button class="toggle" @click="show = !show">{{ show ? "Hide" : "Show" }} chart</button>
  <Chart v-if="show" />
</template>
Lazy hydration with hydrateOnInteraction: server render, then hydrate in jsdom
// Lazy hydration (3.5+): server-render, then hydrate in jsdom with hydrateOnInteraction.
import { JSDOM } from "jsdom";
const { createSSRApp, defineAsyncComponent, hydrateOnInteraction, h, ref, onMounted } = await import("vue");
const { renderToString } = await import("@vue/server-renderer");

let loads = 0;
const Counter = { setup() { const n = ref(0); onMounted(() => console.log("  Counter hydrated (onMounted)")); return () => h("button", { onClick: () => n.value++ }, `clicked ${n.value}`); } };
// A fresh async component per app, so the server and the "browser" each load it themselves.
const makeApp = (where) => {
  const LazyCounter = defineAsyncComponent({
    loader: async () => { loads++; console.log(`  [${where}] loader called`); return Counter; },
    hydrate: hydrateOnInteraction("click"),
  });
  return createSSRApp({ render: () => h("div", [h("p", "static header"), h(LazyCounter)]) });
};

const html = await renderToString(makeApp("server"));
console.log("server HTML:", html);
loads = 0;

const dom = new JSDOM(`<div id="app">${html}</div>`, { url: "http://localhost/" });
for (const k of ["window", "document", "Node", "Element", "HTMLElement", "SVGElement", "Text", "Comment", "Event", "MouseEvent"]) globalThis[k] = dom.window[k];
makeApp("client").mount("#app");
await new Promise((r) => setTimeout(r, 20));
console.log("after hydrate(): loader calls =", loads, "| button:", document.querySelector("button").textContent);
document.querySelector("button").dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }));
await new Promise((r) => setTimeout(r, 20));
console.log("after 1st click : loader calls =", loads, "| button:", document.querySelector("button").textContent);
document.querySelector("button").click();
await new Promise((r) => setTimeout(r, 20));
console.log("after 2nd click : loader calls =", loads, "| button:", document.querySelector("button").textContent);

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 79 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextHard

How do you render very large lists efficiently in Vue (virtual scrolling)?

Continue