Day 29 – Add a Loader and Error State

On day 29, I add a loader (A <div>Loading …</div>) to show that the page is loading the data.

It is incredibly easy in Angular 20 because it is a built-in functionality in httpResource. In Vue 3, I installed vueuse and applied the useFet…


This content originally appeared on DEV Community and was authored by Connie Leung

On day 29, I add a loader (A <div>Loading ...</div>) to show that the page is loading the data.

It is incredibly easy in Angular 20 because it is a built-in functionality in httpResource. In Vue 3, I installed vueuse and applied the useFetch composable to initiate the network requests.

In SvelteKit, a loader can be implemented with the navigating object in $app/state. It also has an error helper function to set the HTTP status code and an error message.

Framework Approach
Vue 3 vueuse's useFetch Composable
SvelteKit navigating and the error helper function
Angular 20 built-in in httpResource

Implement loading and error states in Home

Vue 3 application

Install vueuse/core

npm install --save-exact @vueuse/core

Replace theusePost composable with the useFetch composable.

export const postsUrl = 'https://jsonplaceholder.typicode.com/posts'
export const usersUrl = 'https://jsonplaceholder.typicode.com/users'
<script setup lang="ts">
import PostCard from '@/components/PostCard.vue';
import { postsUrl } from '@/constants/apiEndpoints';
import type { Post } from '@/types/post';
import { useFetch } from '@vueuse/core';

const {
  data: posts,
  isFetching,
  error,
} = useFetch<Post[]>(postsUrl).json()
</script>

The userFetch accepts a URL that is either a string or a ref/shallowRef. The .json() function returns data as a JSON.

isFetching is true when data is being loaded and falose when the loading completes.

error returns any error that occurs during the network request.

<template>
  <div v-if="isFetching" class="text-center mb-10">Loading ...</div>
  <div v-if="error" class="text-center mb-10">{{ error }}</div>
  <div v-if="posts" class="flex flex-wrap flex-grow">
    <p class="ml-2 w-full">Number of posts: {{ posts.length }}</p>
    <PostCard v-for="post in posts" :key="post.id" :post="post" />
  </div>
</template>

When isFetching is true, thediv element displays the Loading... static text. When there is an error, {{ error }} displays the network error. When the endpoint returns the posts successfully, the v-for directive iterates the array and renders the PostCard component.

SvelteKit application

The loading indicator and error message are displayed in +layout.svelte.

<script lang="ts">
    import { page, navigating } from '$app/state';

    let { children } = $props();
</script>
{#if navigating.to}
    <div>Loading page...</div>
{:else if page.error}
    {page.error.message}
{:else}
    <div class="container">
        {@render children?.()}
    </div>
{/if}

When navigation.to is not null, the page is navigating and is loading the data. Therefore, the div element displays the Loading page... static text.

If page.error references an Error object, page.error.message displays the error message.

import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

// retreive all posts
export const load: PageServerLoad = async ({ fetch }) => {
    const postResponse = await fetch(`${BASE_URL}/posts`);

    if (!postResponse.ok) {
        error(404, {
            message: 'Failed to fetch posts'
        });
    }

    const posts = (await postResponse.json()) as Post[];
    return { posts };
};

The load function checks that the response is not ok and the error helper function throws a 404 error with a custom message.

Angular 20 application

import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { PostcardComponent } from '../post/postcard.component';
import { PostsService } from '../post/services/posts.service';

@Component({
  selector: 'app-home',
  imports: [PostcardComponent],
  template: `... inline template ...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {
  postService = inject(PostsService);

  postsRes = this.postService.posts;

  posts = computed(() => (this.postsRes.hasValue() ? this.postsRes.value() : []));

  error = computed<string>(() =>
    this.postsRes.status() === 'error' ? 'Error loading the posts.' : '',
  );
}

When the postsRes resource's status is error, the error computed signal returns a custom error message. Otherwise, the error message is an empty string.

@if (postsRes.isLoading()) {
  <div>Loading...</div>
} @else if (error()) {
  <div>Error: {{ error() }}</div>
}
@if (posts(); as allPosts) {
    <div class="flex flex-wrap flex-grow">
      <p class="ml-2 w-full">Number of posts: {{ allPosts.length }}</p>
      @for (post of allPosts; track post.id) {
        <app-postcard [post]="post" />
      }
    </div>
}

When postsRes.isLoading() is true, the div element displays the Loading... static text.

When the error computed signal is evaluated to true, the error message is displayed.

When allPosts is defined, the template displays the number of posts and iterates the array to render the PostComponent.

Implement loading and error states in Post

Vue 3 application

First, the useFetch composable retrieves a post by the path param. This is straightforward because this url depends on path param only and it can be destructured from the useRoute composable.

<script setup lang="ts">
import { postsUrl, usersUrl } from '@/constants/apiEndpoints'
import type { Post } from '@/types/post'
import type { User } from '@/types/user'
import { useFetch } from '@vueuse/core'
import { computed, shallowRef, watch } from 'vue'
import { useRoute } from 'vue-router'

const { params } = useRoute()
const url = `${postsUrl}/${params.id}`

const { 
    data: post, 
    isFetching: isFetchingPost, 
    error: errorPost 
} = useFetch<Post>(url).json()
</script>

When post is retrieved, use the useFetch composable to retrieve the user by the post's user ID.

The user url changes when the user ID changes, so it is a shallowRef.

const userUrl = shallowRef('')

const {
  data: user,
  isFetching: isFetchingUser,
  error: errorUser,
} = useFetch<User>(userUrl, { refetch: true, immediate: false }).json()

The refetch: true means that a new request is made when userUrl changes. Moreover, the userUrl is initially a blank string, so I don't want it to fire immediately. The final useFetchOptions is:

{
     refetch: true,
     immediate: true,
}

Modify the watcher to track the post and update the userUrl shallowRef programmatically.

watch(
  () => ({ ...post.value }),
  ({ userId = undefined }) => (userUrl.value = userId ? `${usersUrl}/${userId}` : ''),
)

When userUrl is not blank, the useFetch composable automatically retrieves a user by the user ID.

const isFetching = computed(() => isFetchingPost.value || isFetchingUser.value)

Add the isFetching computed ref to display a loader when post or user is being loaded.

const error = computed(() => {
  if (errorPost.value) {
    return errorPost instanceof Error ? errorPost.message : 'Error retrieving a post.'
  }

  if (errorUser.value) {
    return errorUser instanceof Error ? errorUser.message : 'Error retrieving a user.'
  }

  return ''
})

Add the error computed ref to display any error message.

<template>
  <div v-if="isFetching" class="text-center my-10">Loading...</div>
  <div v-if="error" class="text-center my-10">{{ error }}</div>
  <div v-if="post && user" class="mb-10">
    <h1 class="text-3xl">{{ post.title }}</h1>
    <div class="text-gray-500 mb-10">by {{ user.name }}</div>
    <div class="mb-10">{{ post.body }}</div>
  </div>
</template>

Display a loader when isFetching is true and an error message when it is not blank. The template displays the post and user name after both data is loaded successfully.

SvelteKit application

import { BASE_URL } from '$lib/constants/posts.const';
import type { Post } from '$lib/types/post';
import type { PostWitUser, User } from '$lib/types/user';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

// retreive a post by an ID
export const load: PageServerLoad = async ({ params, fetch }): Promise<PostWitUser> => {
    const post = (await retrieveResource(fetch, `posts/${+params.id}`, 'Post')) as Post;
    const user = (await retrieveResource(fetch, `users/${post.userId}`, 'User')) as User;

    return {
        post,
        user,
    };
};

The retrieveResource helper function uses the native fetch function to retrieve the post by the post ID. When the post is retrieved successfully, this helper function uses the post's user ID to retrieve the user. Next, the load function returns both the post and user to +page.svelte.

type FetchFunction = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;

async function retrieveResource(fetch: FetchFunction, subPath: string, itemName: string) {
    const url = `${BASE_URL}/${subPath}`;
    const response = await fetch(url);
    if (!response.ok) {
        error(404, {
            message: `Failed to fetch ${itemName}`
        });
    }
    const item = await response.json();

    if (!item) {
        error(404, {
            message: `${itemName} does not exist`
        });
    }

    return new Promise((resolve) => {
        setTimeout(() => resolve(item), 1000);
    });
}

The retrieveResource fetches the item and examines the response. When the response is not ok, it throws a 404 error with the custom message, Failed to fetch ${itemName}. await response.json() resolves the Promise to an object. If the object is undefined, it also throws a 404 error with the custom message, ${itemName} does not exist.

return new Promise((resolve) => {
    setTimeout(() => resolve(item), 1000);
});

The promise creates a delay of one second to simulate the loading behavior.

Angular 20 application

import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
import { UserService } from './services/user.service';
import { Post } from './types/post.type';

@Component({
  selector: 'app-post',
  styles: `
    @reference "../../styles.css";

    :host {
      @apply flex m-2  gap-2 items-center w-1/4 flex-grow rounded overflow-hidden w-full;
    }
  `,
  template: `... inline template ...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class PostComponent {
  readonly userService = inject(UserService);

  post = input<Post>();

  userRef = this.userService.createUserResource(this.post);

  user = computed(() => (this.userRef.hasValue() ? this.userRef.value() : undefined));

  error = computed<string>(() =>
    this.userRef.status() === 'error' ? 'Error loading the post.' : '',
  );
}
@let myUser = user();
@let myPost = post();
@if (userRef.isLoading()) {
  <div>Loading...</div>
} @else if (error()) {
  <div>Error: {{ error() }}</div>
} @else if (myPost && myUser) {
  <div class="mb-10">
    <h1 class="text-3xl">{{ myPost.title }}</h1>
    <div class="text-gray-500 mb-10">by {{ myUser.name }}</div>
    <div class="mb-10">{{ myPost.body }}</div>
  </div>
} @else {
  <div>Post not found</div>
}

When userRef.isLoading() is true, the div element displays the Loading... static text.

When the error computed signal is evaluated to true, the error message is displayed.

The last elseif displays the post title, post body and user name.

We have successfully implemented a simple loader and an error indicator on each page.

Github Repositories

Resources


This content originally appeared on DEV Community and was authored by Connie Leung


Print Share Comment Cite Upload Translate Updates
APA

Connie Leung | Sciencx (2025-11-09T05:37:46+00:00) Day 29 – Add a Loader and Error State. Retrieved from https://www.scien.cx/2025/11/09/day-29-add-a-loader-and-error-state/

MLA
" » Day 29 – Add a Loader and Error State." Connie Leung | Sciencx - Sunday November 9, 2025, https://www.scien.cx/2025/11/09/day-29-add-a-loader-and-error-state/
HARVARD
Connie Leung | Sciencx Sunday November 9, 2025 » Day 29 – Add a Loader and Error State., viewed ,<https://www.scien.cx/2025/11/09/day-29-add-a-loader-and-error-state/>
VANCOUVER
Connie Leung | Sciencx - » Day 29 – Add a Loader and Error State. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/09/day-29-add-a-loader-and-error-state/
CHICAGO
" » Day 29 – Add a Loader and Error State." Connie Leung | Sciencx - Accessed . https://www.scien.cx/2025/11/09/day-29-add-a-loader-and-error-state/
IEEE
" » Day 29 – Add a Loader and Error State." Connie Leung | Sciencx [Online]. Available: https://www.scien.cx/2025/11/09/day-29-add-a-loader-and-error-state/. [Accessed: ]
rf:citation
» Day 29 – Add a Loader and Error State | Connie Leung | Sciencx | https://www.scien.cx/2025/11/09/day-29-add-a-loader-and-error-state/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.