Handling Loading & Error States
Using the pending, error, and status fields from useFetch to build interfaces that don't just freeze while data loads.
2 min de lecture
useFetch and useAsyncData don't just return data — they return reactive state describing the request itself, which is what lets you build a loading spinner or an error message without wiring up your own flags by hand.
The fields you get back
<script setup>
const { data, pending, error, status, refresh } = await useFetch("/api/posts");
</script>data— the result once the request succeeds (nullbefore then).pending—truewhile the request is in flight.error— set if the request failed; otherwisenull.status— one of"idle","pending","success", or"error", useful when a single boolean doesn't capture what you need.refresh— re-runs the same request on demand.
<template>
<div v-if="status === 'pending'">Loading posts…</div>
<div v-else-if="status === 'error'">
Couldn't load posts. <button @click="refresh">Try again</button>
</div>
<ul v-else>
<li v-for="post in data" :key="post.id">{{ post.title }}</li>
</ul>
</template>Awaited vs. non-awaited calls
Whether pending is ever true on first render depends on whether you await the call. Awaiting it (as above) blocks navigation to the page until the data is ready — the page won't render at all until the fetch resolves, so pending never actually shows during SSR, only on a later refresh().
<script setup>
// Not awaited — page renders immediately, pending starts true
const { data, pending } = useFetch("/api/posts");
</script>Skip the await when you'd rather show the rest of the page immediately and let this one section load in — a comments list below an already-visible article, for example. Use await when the page genuinely has nothing useful to show until the data arrives.
Handling errors from the server honestly
$fetch and useFetch throw (or populate error) on any non-2xx response, so a failed API call doesn't silently render with data as null — you have to handle it explicitly:
<script setup>
const { data: user, error } = await useFetch(`/api/users/${route.params.id}`);
if (error.value?.statusCode === 404) {
throw createError({ statusCode: 404, statusMessage: "User not found" });
}
</script>createError (auto-imported) triggers Nuxt's built-in error page instead of rendering a broken page with missing data — the right call whenever the error means the page has nothing valid to show at all, rather than just one section of it.
Why this matters beyond polish
Skipping loading and error states doesn't just look unfinished — during SSR, an unhandled rejection from a data-fetching composable can crash the entire server-rendered response for that request. Treating pending and error as required parts of any useFetch call, not optional extras, is what keeps a slow or failing API from taking the whole page down with it.