The problem is that queries in the forkjoin array continue to execute even if the previous one failed. For example, already in the first request putCategory$ an error occurred (the name already exists). However, the next loadImage$ request is executed and the new image is still loaded. Why? How to make a sequence of requests so that if a failure occurs, the next ones will not be executed?
PUTCategory(category: ICategory) {
const newImage = category.image;
const oldImage = this.editedCategory.image;
const imageChanged = oldImage !== newImage;
const loadImage = imageChanged && !!newImage;
const deleteImage = imageChanged && !!oldImage;
const file: File = this.form.value.image;
const putCategory$ = this.categoryService.putCategory(category).pipe(
catchError((response: any) => {
if (response.error.info == 'NAME_EXISTS') {
this.throwErrorModal(
'Категория с таким названием уже существует. Измените название и попробуйте снова.',
);
} else {
this.throwErrorModal(
'Произошла ошибка при попытке обновить категорию',
);
}
return EMPTY;
}),
);
const loadImage$ = loadImage
? this.categoryService.uploadCategoryImage(file).pipe(
catchError(() => {
this.throwErrorModal(
'Произошла ошибка при попытке загрузить файл нового изображения категории',
);
return EMPTY;
}),
concatMap((response: any) => {
const filename = response.filename;
return this.categoryService
.setImageToCategory(category.id, filename)
.pipe(
catchError(() => {
this.throwErrorModal(
'Произошла ошибка при попытке связать новое загруженное изображение и категорию',
);
return EMPTY;
}),
);
}),
)
: of(null);
const deleteImage$ = deleteImage
? this.categoryService.deleteImage(oldImage!).pipe(
catchError(() => {
this.throwErrorModal(
'Произошла ошибка при попытке удалить старое изображение категории',
);
return EMPTY;
}),
concatMap(() => {
return this.categoryService
.setImageToCategory(category.id, '')
.pipe(
catchError(() => {
this.throwErrorModal(
'Произошла ошибка при попытке удаления связи старого изображения с категорией',
);
return EMPTY;
}),
);
}),
)
: of(null);
forkJoin([putCategory$, deleteImage$, loadImage$])
.pipe(
finalize(() => {
this.awaitModal = false;
this.cdr.markForCheck();
}),
tap(() => {
this.successModal = true;
}),
)
.subscribe();
}
Just in case, throwErrorModal is my simple function
private throwErrorModal(content: string) {
this.errorModalContent = content;
this.errorModal = true;
}
forkJoin
waits for all observables to emit a value or errors and completes.
You are using catchError
to catch any error from the API, and this is the reason why forkJoin
doesn't unsubscribe from the other streams.
You could throw
a new error after having displayed the modal so that the other streams are blocked.
const putCategory$ = this.categoryService.putCategory(category).pipe(
catchError((response: any) => {
if (response.error.info == 'NAME_EXISTS') {
this.throwErrorModal(
'Категория с таким названием уже существует. Измените название и попробуйте снова.',
);
} else {
this.throwErrorModal(
'Произошла ошибка при попытке обновить категорию',
);
}
throw new Error("error message")
}),
);
Keep in mind that using forkJoin
, the 3 API calls will be executed asynchronously, thus you cannot know if one completes (or errors) before the other. If you use it there could be a scenario where deleteImage$
or loadImage$
complete before putCategory$
has the chance to throw an error.
So if the other observables needs to depend on the result of putCategory$
, you could pipe the latter, and return the forkJoin
of the formers in a switchMap
:
putCategory$.pipe(
// Here `putCategory` has done its job
switchMap( () => forkJoin([deleteImage$, loadImage$])),
// Here the other observables have completed and have been executed asynchronously
// But only if putCategory hasn't thrown any errors
finalize(() => {
this.awaitModal = false;
this.cdr.markForCheck();
}),
tap(() => {
this.successModal = true;
}),
)