EN VI

Angular - rxjs forkJoin does not stop subsequent requests after an error?

2024-03-14 04:00:13
How to Angular - rxjs forkJoin does not stop subsequent requests after an error

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;
  }

Solution:

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;
  }),
)

Answer

Login


Forgot Your Password?

Create Account


Lost your password? Please enter your email address. You will receive a link to create a new password.

Reset Password

Back to login