9

Suppose I have some operation that returns an observable.

this.loginService
  .post<IResponse>(url, { username, password })
  .subscribe(
    next => { /* I know the type of next */ },
    error => { /* Can I specify type of err, or is it always any? */ }
  );

The type of next is something I can set - in this case IResponse.

What about error - can I specify it's type somehow? Or is it always any, and I'll always need to typecast within the handler?

2
  • 1
    You can never assume the type of an error caught by an observable. You can use instantof operator to check the type at run time. What I do is catch all errors in a service and then only forward errors of a given type. Consumers of that service can then assume the error type downstream. Commented Jun 10, 2019 at 11:48
  • @Reactgular That was what I initially assumed until I saw the answer below - do you maybe have an example of that? I also prefer not to assume a type, it feels like I'm breaking the type system. Commented Jun 10, 2019 at 11:53

4 Answers 4

3

It's been my experience that a service that yields observables from a HTTP operation should never emit uncaught HTTP errors. It creates a lot of boilerplate code in the consumers of that service, and forces consumers to handle HTTP specific issues.

I also think that services should not catch all types of errors, but only the known and expected errors. Requiring the application itself to handle edge cases using an error interceptor.

export interface ILoginResponse {
   //...
}

export interface IServiceResponse<TType> {
   success: boolean;
   payload?: TType;
}

@Injectable()
export class LoginService {

   public login(options: any): Observable<IServiceResponse<ILoginResponse>> {
      return this.httpClient.post<ILoginResponse>('login', options).pipe(
          map(payload => ({success: true, payload})),
          catchError(err => {
              if(err instanceof HttpErrorResponse) {
                  // handle http errors here, maybe retry, update headers, etc..
                  return of({success: false});
              }
              // this error is unexpected, we don't know what to do
              // let the app interceptor handle it, but upstream observables
              // won't know what to do either. Our app has crashed...
              // You can return success false, but why handle this error?
              return throwError(err);
          });
   }
}

Why should you do the above?

Any time you use a route resolver that emits an uncaught error it will fail route transition. The router will basically crash. The user will basically just see a blank page, because resolvers have no error handling fall back.

The above only catches HTTP errors, because that's the only kind that we think should be thrown. Another kind of error should be seen as a bug.

If a component calls this service, then the programmer should check the success flag for true before trusting the emitted value. This can be filtered if the business logic does not care if it fails, or the component can visually show the user that it failed to perform the HTTP request. Either way, the HTTP error response does not go to the app error interceptor.

This is just an approach. People do their HTTP stuff all differently. The other answer correctly answers your question.

Sign up to request clarification or add additional context in comments.

2 Comments

@lonix You should consider these suggestions
I think this is bad advice.What you are doing is just turning the error state into a boolean success state. Consumers still need to handle that boolean flag, and you have created more boilerplate interfaces that include the flag. The second reason is also wrong. Resolvers are there to provide data that is needed to display the view. If the data is not there, such as in the case of a success = false, the view cannot be displayed. You can (and should) still manage the observable error in the resolver with a catchError and redirect to an appropriate view.
3

You can use parentheses for what you want to achieve:

this.loginService
  .post<IResponse>(url, { username, password })
  .subscribe(
    (next: TypeOfYourResponse) => {...},
    (error: TypeOfYourErrorResponse) => {...}
  );

For further reading on arrow functions:

https://www.tutorialsteacher.com/typescript/arrow-function

https://basarat.gitbooks.io/typescript/docs/arrow-functions.html


Update 2024

As RxJS deprecated all signatures of subscribe() that take more than 1 argument since v6.4, it needs to be as following:

this.loginService
  .post<IResponse>(url, { username, password })
  .subscribe({
    next: (response: TypeOfYourResponse) => {...},
    error: (err: TypeOfYourErrorResponse) => {...}
  });

4 Comments

Ahh nice didn't think of that. BTW, if it's the wrong type, will it throw an error or just carry on?
afaik, it not for runtime as JavaScript does not have types on runtime. But it is for compile-time. I'm not 100% sure btw
If its the wrong type, it will silently carry on and probably fail at runtime. This is the general rule when an unchecked any is involved.
The proper typehint is (error: any) => {...}. One can use instanceof if more fine grain error handling is required.
2

Another way to do it using catchError operator:

this.loginService
  .post<IResponse>(url, { username, password })
  .catchError<TypeOfYourErrorResponse>(() => // Your catch callback)
  .subscribe(
    next => { /* I know the type of next */ },
  );

1 Comment

I can't remember, but I think you have to define two generic parameters for catchError. Maybe the error type and the returned type. I'm not sure the signature accepts a single parameter.
0

I really liked @Reactgular's accepted answer

I like the fact it will make the consumer think about errors when they have to check success status! However sometimes the consumer may want to get and display an error from the server, so I've changed it a little:

// DON'T FORGET THESE!
import { Observable, of } from "rxjs";
import { catchError, map } from "rxjs/operators";

export interface ILoginResponse {
   //...
}

export interface IServiceResponse<TType> {
   payload?: TType;
   error?: HttpErrorResponse;
}

@Injectable()
export class LoginService {

   public login(options: any): Observable<IServiceResponse<ILoginResponse>> {
      return this.httpClient.post<ILoginResponse>('login', options).pipe(
          map(payload => ({ payload })),
          catchError(err => of({ error: err }))
   }
}

No need for success flag, just check if you have a payload or error

loginService.login().subscribe(response => {
   if (response.payload) {
      // success...
   }
   else if (response.error) {
      // handle the error
   }
},

I think return all errors, the client might not care so it can be ignored, but it will be there if they do care; if you want to handle specific errors, use an HttpInterceptor somewhere else...

1 Comment

ILoginResponse and catchError is just needless boilerplate. Subscriptions already allow catching errors. You just moved the error into a new interface. In the end, two code paths are necessary regardless. Why reinvent the wheel?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.