I found myself repeatedly writing the same code to notify users of the status of asynchronous tasks — whether the task was still in progress, completed successfully, or had failed.
In this post, I’ll show you how to create a TaskStatusNotifierService
to handle these notifications, reducing the amount of repetitive code and letting you focus on the task’s actual logic.
I'll use Ionic in my example, but you can modify the service to work with any Angular project.
The Problem
In every component, I kept doing the following:
- Injecting these three controllers:
-
LoadingController
(to show a spinner while the task runs) -
ToastController
(to show a message when the task completes) -
AlertController
(to show an alert if the task fails)
2. Using the controllers:
- Create and present a loader before the async task starts, and wait for it to appear on the screen.
- Run the task, then dismiss the loader and wait for it to disappear.
- After dismissing the loader, show a toast if the task was successful, or an alert if it failed.
All the Ionic controllers are asynchronous and follow a create/present/dismiss pattern, which adds more code.
The code looked like this:
@Component({})
class MyComponent {
constructor(
private loadingController: LoadingController,
private toastController: ToastController,
private alertController: AlertController,
private apiService: ApiService
) {}
async save(data: any) {
const loader = await this.loadingController.create({
message: "Saving..."
});
await loader.present();
try {
await this.apiService.save(data);
await loader.dismiss();
const toast = await this.toastController.create({
message: "Saved successfully",
duration: 3000,
});
await toast.present();
} catch (error) {
await loader.dismiss();
const alert = await this.alertController.create({
header: "Failed to save",
message: error.message,
buttons: ["OK"],
});
await alert.present();
}
}
}
Even in this simple example, you can see that the code is repetitive and hard to maintain. If I want to change how I display the loader, toast, or alert globally, I'd have to update every component.
The Solution
Let's create a TaskStatusNotifierService
that contains all the repetitive code. This service has a method called run
that takes a task and runs it.
Now, I only need to inject the TaskStatusNotifierService
in my components and call the run
method with the task I want to execute:
@Component({})
class MyComponent {
constructor(
private taskStatusNotifierService: TaskStatusNotifierService,
private apiService: ApiService
) {}
save(data: any) {
this.taskStatusNotifierService.run(() => this.apiService.save(data));
}
}
The TaskStatusNotifierService
handles showing the loader, running the task, and showing the toast or alert when the task finishes.
Here's how the TaskStatusNotifierService
looks:
@Injectable({ providedIn: "root" })
export class TaskStatusNotifierService {
constructor(
private loadingController: LoadingController,
private toastController: ToastController,
private alertController: AlertController
) {}
async run(task: () => Promise<any>) {
const loader = await this.loadingController.create({
message: "Loading..."
});
await loader.present();
try {
await task();
await loader.dismiss();
const toast = await this.toastController.create({
message: "Task completed successfully",
duration: 3000,
});
await toast.present();
} catch (error) {
await loader.dismiss();
const alert = await this.alertController.create({
header: "Task failed",
message: error.message,
buttons: ["OK"],
});
await alert.present();
}
}
}
Using Observables
If your apiService.save
returns an Observable instead of a Promise, you can convert it using lastValueFrom
from rxjs
:
import { lastValueFrom } from "rxjs";
@Component({})
class MyComponent {
constructor(
private taskStatusNotifierService: TaskStatusNotifierService,
private apiService: ApiService
) {}
save(data: any) {
this.taskStatusNotifierService.run(() =>
lastValueFrom(this.apiService.save(data))
);
}
}
Why lastValueFrom
?
Some Observables may emit multiple values. In our TaskStatusNotifierService
, we care about the last value. We don't want to show a success message and dismiss the loader on the first emission if there are more to come, so we use lastValueFrom
.
Customizing Messages
We can update the service to allow customizing the loading, success, and error messages by passing them as parameters:
@Component({})
class MyComponent {
constructor(
private taskStatusNotifierService: TaskStatusNotifierService,
private apiService: ApiService
) {}
save(data: any) {
this.taskStatusNotifierService.run(() => this.apiService.save(data), {
loadingMessage: "Saving...",
successMessage: "Saved successfully",
errorMessage: "Failed to save",
});
}
}
Update the TaskStatusNotifierService
to accept these parameters:
@Injectable({ providedIn: "root" })
export class TaskStatusNotifierService {
constructor(
private loadingController: LoadingController,
private toastController: ToastController,
private alertController: AlertController
) {}
async run(
task: () => Promise<any>,
options: {
loadingMessage?: string;
successMessage?: string;
errorMessage?: string;
} = {}
) {
const loadingMessage = options.loadingMessage || "Loading...";
const successMessage =
options.successMessage || "Task completed successfully";
const errorMessage = options.errorMessage || "Task failed";
const loader = await this.loadingController.create({
message: loadingMessage
});
await loader.present();
try {
await task();
await loader.dismiss();
const toast = await this.toastController.create({
message: successMessage,
duration: 3000,
});
await toast.present();
} catch (error) {
await loader.dismiss();
const alert = await this.alertController.create({
header: errorMessage,
message: error.message,
buttons: ["OK"],
});
await alert.present();
}
}
}
If you need more control over the messages, you can pass functions that generate messages based on the result or error:
@Component({})
class MyComponent {
constructor(
private taskStatusNotifierService: TaskStatusNotifierService,
private apiService: ApiService
) {}
save(data: any) {
this.taskStatusNotifierService.run(() => this.apiService.save(data), {
loadingMessage: "Saving...",
getSuccessMessage: (result) => `Saved item. ID: ${result.id}`,
getErrorMessage: (error) => `Something went wrong: ${error.message}`,
});
}
}
Update the TaskStatusNotifierService
accordingly:
@Injectable({ providedIn: "root" })
export class TaskStatusNotifierService {
constructor(
private loadingController: LoadingController,
private toastController: ToastController,
private alertController: AlertController
) {}
async run(
task: () => Promise<any>,
options: {
loadingMessage?: string;
getSuccessMessage?: (result: any) => string;
getErrorMessage?: (error: any) => string;
} = {}
) {
const loadingMessage = options.loadingMessage || "Loading...";
const getSuccessMessage =
options.getSuccessMessage || (() => "Task completed successfully");
const getErrorMessage = options.getErrorMessage || (() => "Task failed");
const loader = await this.loadingController.create({
message: loadingMessage
});
await loader.present();
try {
const result = await task();
await loader.dismiss();
const toast = await this.toastController.create({
message: getSuccessMessage(result),
duration: 3000,
});
await toast.present();
} catch (error) {
await loader.dismiss();
const alert = await this.alertController.create({
header: getErrorMessage(error),
message: error.message,
buttons: ["OK"],
});
await alert.present();
}
}
}
You can even extend the service to allow full control over the success and error modals by passing functions that return options for the controllers.
With this TaskStatusNotifierService
, you can simplify your code and focus on what really matters—the logic of your tasks. Feel free to expand and customize the service to suit your needs. It's a great starting point for handling asynchronous tasks in your Angular projects.
Top comments (0)