6
votes

I'm using Angular and rxjs 5.x.x . For each http request I must log the response in a centralized place -either if success or fail.

The problem which i'm facing is in a cituation where a catch clause is being called.

For example :

  callRemote() {
    return this.http.get("http://aaa.dom")
                    .catch(f=>Rx.Observable.of(f))
                    .do(f => console.log("logging response both bad and ok..."))
  }

This is an http request which fails. But I must invoke the do operator becuase I need to log it. However if a .catch is called , the .do won't be called. That's why I did this :

  .catch(f=>Rx.Observable.of(f))

So I'm basically catching the error and wrap it as a new observable and continue to the .do function.

So far so good.

The problem now is that ,eventually , when subscribing , I want to know if request failed or OK.

So basically this is how I subscribe :

 this._http.callRemote().subscribe(
      (x => console.log('OK')),
      (x => console.log('ERROR')),
      (() => console.log('COMPLETE'))
    );

Problem is that everything is redirected to the sucess callback function. Even failures. I do understand why it happend - it's becuase I wrapped catch fails as new Observables.

Question

Is it possible that wrapped errors will go to the error callback , and not to the success callback ?

Online demo

1
If you want it to continue being an error, why don't you Observable.throw it instead of turning it into a successful stream? Just log it where you catch it instead. - jonrsharpe
@jonrsharpe you mean .catch(f=>Rx.Observable.throw (f)) ? If I do that - then do is not called. - Royi Namir
Yes, exactly. Otherwise you could e.g. wrap it in some kind of object like { error, result } and then unwrap it later, which seems kind of awkward. - jonrsharpe
The catch shouldn't swallow the error in the first place, thats a nogo for sure. Eventually you could do your logging in the catch and then rethrow, but thats kinda overkill for only log side effect. Simplest option would be to use the error callback in the do operator. - Jota.Toledo
@Jota.Toledo What do you mean by use the error callback in the do operator ? - Royi Namir

1 Answers

15
votes

As already mentioned in the comments, the catch shouldn't swallow the error in the first place, thats a nogo for sure. By doing that you are simply blocking the error signal in the rxjs chain.

Eventually you could do your logging in the catch and then re throw, but thats kinda overkill for only log side effect.

The simplest option would be to use the error callback arguent in the do operator.

According to this tutorial page, the do operator can take 3 arguments (callback fns), what basically matches the subscribe signature:

1s argument: callback on next

2nd argument: callback on error

3rd argument: callback on complete

So you could refactor into the following:

callRemote() {
    return this.http.get("http://aaa.dom")
                    .do(
                      response => console.log("logging response both bad and ok..."), 
                      error => console.log("Something exploded, call 911");
  }

So basically you could attach that operation to every single HttpClient call in your base code. Pretty neat, no?

Wait wait! This might look neat on a first glance, but it will backfire at the very same moment that you want to:

  • Modify the logging behavior
  • Refactor in any way

Why?

You are going to basically monkey-patch every single possible back end call with that do() operation. And if making 1 change to that logic means changing code in more than 3 places, there something smelly going on.

A better approach

With the introduction of the HttpClient, another API was added: the HttpInterceptor API.

Basically, you could intercept all your outgoing requests in a single point.

How? As it follows:

First step, create an injectable service that you can use to capsulate the logging logic;

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { AuthService } from './auth/auth.service';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators'; // fancy pipe-able operators

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
       tap(
           response => console.log("Oh boy we got an answer"), 
           error => console.log("Something might be burning back there")
       ));
  }
}

Second step, make angular aware of the existence of the LoggingInterceptor by providing it through a token:

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule, ClassProvider } from '@angular/core';

import { LoggingInterceptor } from '../some-place';

const LOGGING_INTERCEPTOR_PROVIDER: ClassProvider = {
   provide: HTTP_INTERCEPTORS ,
   useClass: LoggingInterceptor,
   multi: true
};

@NgModule({
   ...
   providers: [
      LOGGING_INTERCEPTOR_PROVIDER
   ]
   ...
})
export class AppModule {}

And thats it! Now you can log all your outgoing requests and do some other cool stuff with those logs if necessary, in a truly centralized way.