Home > Software design >  Handle error inside ngSubmit of template form (thrown by synchronous non-http service)
Handle error inside ngSubmit of template form (thrown by synchronous non-http service)

Time:12-24

I'm starting with Angular and trying to KISS as much as possible. I have a form in a template whose (ngSubmit) component method calls an injected Service method that calls a synchronous back-end (not using HTTP, everything happens in memory in a domain layer) which can throw exceptions.

For example, if trying to start a game with a player's name that already exists, the domain layer will tell me with an AlreadyExistsError on the method to start a new game.

The exception is caught just fine with try/catch as so:

onSubmit() {
    try {
      this.myService.methodCallingSyncBackEnd(this.model.name); // player name from form
      this.submitted = true;
    } catch (error) {
      console.log(error);
      if (error instanceof AlreadyExistsError)
        this.errorMsg = error.message;  // player already exists
      else
        this.errorMsg = 'Unknown error.';
    }
  }

The catch is working. However, the front-end seems to hang (the Submit button looks like it's not had a button-up - see the bold outline), and I also don't see the {{errorMsg}} in the HTML template.

enter image description here

The console log for error shows:

Error: Player 'Marie Antoinette' already exists.
    at Game.startGame (Game.ts:28)
    at MyService.methodCallingSyncBackEnd (my-service.service.ts:20)
    at PlayComponent.onSubmit (play.component.ts:27)
    at PlayComponent_Template_form_ngSubmit_4_listener (play.component.html:5)
    at executeListenerWithErrorHandling (core.mjs:14949)
    at Object.wrapListenerIn_markDirtyAndPreventDefault [as next] (core.mjs:14987)
    at Object.next (Subscriber.js:110)
    at SafeSubscriber._next (Subscriber.js:60)
    at SafeSubscriber.next (Subscriber.js:31)
    at Subject.js:31
  • Game.ts is the domain class that detects the error and throws the exception with a message. It's all in memory (nothing asynchronous).
  • my-service.service.ts is an Angular service injected into the PlayComponent that has the form template defined in its .html.

This looks like I need some kind of framework-friendly way to handle errors withfrom a call to an injected service, but I don't want to complicate my code with Observables/Observers if I don't have to, because (so far) I don't need to have much reactivity in my application.

Since there's no web back-end, and try/catch is such straightforward code (easier to understand), I would like to keep it simple. Is there something I can do to "unwind" properly the framework from the catch, or am I stuck using some more complicated code because of ngForm/ngSubmit?

CodePudding user response:

You can use catchError of rxjs

this.myService.methodCallingSyncBackEnd(this.model.name).pipe(catchError((error) => {
if (error instanceof AlreadyExistsError) {
 this.errorMsg = error.message;
}
}),
);

https://www.learnrxjs.io/learn-rxjs/operators/error_handling/catch

CodePudding user response:

If you don't want to "complicate your code", this solution seems to be not bad. But if you want a "framework-friendly" solution, Angular essentially works with Observables/Observers.

The main advantage to use Observables/Observers is to respect a full asynchronous workflow in all your Angular components. If later a service method will required such delay (API call, processing...), everything will be clean for you.

On your service side, you can easily implement an Observable like this:

methodCallingBackend(name: string): Observable<void> {
  return new Observable(observer => {
    // ... your code here ...

    // If an error occurred:
    throw new AlreadyExistsError();

    // If everything worked well:
    observer.next();
  });
}

On your component side, you can easily debounce input to provide user an immediate feedback:

this.form.get('name')?.valueChanges.pipe(debounceTime(400), distinctUntilChanged())
  .subscribe((name) => {
    this.myService.methodCallingBackend(name)
      .pipe(take(1), catchError((error) => {
        //    ^  take(1): Unsubscribe automatically after response

        if (error instanceof AlreadyExistsError) {
          this.errorMsg = error.message;
        }
      }))
      .subscribe(() => {
        // Everything worked well
      });
  });

Like this, your service method will be triggered after each pause of 400ms in user input.

  • Related