How to handle multiple catchError in Observable? - rxjs

First time using NestJs with Observables. I'm familiar with how to solve this using Promises, but I'd like to expand my knowledge of Observables.
Below is a "signup" function that needs to do a couple of things. First, it creates the tenant, and then from the result of the tenant, it creates the user.
In the event that the tenant or user already exists a unique constraint violation error will be raised. What I'd like to do in the event the error occurs is bail out of the observable and return the error to the controller. What ends up happening upon catching the first error it will be caught again in the second "catchError"
I've handled this in the second "catchError" below (by looking for a BadRequestException), but this doesn't feel right. My question is if there is a better way to handle this?
Thanks in advance!
signup(signUpDto: SignupRequestDto, tenantDto: TenantDto): Observable<User> {
const createTenant$ = from(
this.createTenant(tenantDto.name, tenantDto.key),
);
const user = createTenant$.pipe(
catchError((error, caught) => {
const err = error as PrismaClientKnownRequestError;
this._logger.error({ err, caught }, 'tenant err');
// Unique constraint violation
if (err.code === 'P2002') {
throw new BadRequestException(
'Tenant already exists',
tenantDto.name,
);
} else {
throw new InternalServerErrorException('Error creating user');
}
}),
switchMap((tenant) => {
return from(this.createUser(signUpDto, tenant.id));
}),
catchError((error, caught) => {
// If this is error is already handled by the catchError above
if (error instanceof BadRequestException) {
throw error;
} else {
const err = error as PrismaClientKnownRequestError;
this._logger.error({ err, caught }, 'user err');
// Unique constraint violation
if (err.code === 'P2002') {
throw new BadRequestException('Email already exists');
} else {
throw new InternalServerErrorException('Error creating user');
}
}
}),
);
return user;
}

You can achieve it by using single catchError,but the problem was to identify the source.For that I have taken a private variable to identify the source.
private currentEvent: string = 'createTenant'; //default value createTeanant
private currentEvent: string = 'createTenant';
signup(signUpDto: SignupRequestDto, tenantDto: TenantDto): Observable<User> {
const createTenant$ = from(
this.createTenant(tenantDto.name, tenantDto.key)
);
return user = createTenant$.pipe(
switchMap((tenant) => {
currentEvent = 'createUser';//Assigning new event name
return from(this.createUser(signUpDto, tenant.id));
}),
catchError((error, caught) => {
const err = error as PrismaClientKnownRequestError;
this._logger.error({ err, caught }, this.currentEvent + ' err');
// Unique constraint violation
if (err.code === 'P2002' && this.currentEvent === 'createTenant') {
throw new BadRequestException(
'Tenant already exists',
tenantDto.name
);
} else if (err.code === 'P2002' && this.currentEvent === 'createUser') {
throw new BadRequestException('Email already exists');
} else {
throw new InternalServerErrorException('Error creating user');
}
})
);
}

Related

issue when cypress executes code asynchronously

i have this method that returns an object. if i run the code below i find that the second console.log() hits first and the object returns undefined
private routeAndPassengerDataObject: undefined | RouteAndPassenger;
public getDataFromRouteAndPassengerObject(fileName?: string): RouteAndPassenger {
if (this.routeAndPassengerDataObject) {
return this.routeAndPassengerDataObject;
}
if (typeof fileName !== 'string') {
throw new Error(`${fileName} has to be a string`);
}
cy.fixture(fileName).then((data: unknown) => {
if (!isValidRouteAndPassengerObject(data)) {
throw new Error(`${data} is not valid`);
}
console.log(`this is first`);
this.routeAndPassengerDataObject = data;
});
console.log(`this is second`);
return this.routeAndPassengerDataObject!;
}
}
const routeAndPassengerData = getDataFromRouteAndPassengerObject()
console.log(routeAndPassengerData);
result-
this is second
this is first
undefined
would like to know how to handle this please.
returing the object as cypress.chainable like below has worked for me-
public getDataFromRouteAndPassengerObject(fileName?: string): Cypress.Chainable<RouteAndPassenger> {
if (this.routeAndPassengerDataObject) {
return cy.wrap(this.routeAndPassengerDataObject);
}
if (typeof fileName !== 'string') {
throw new Error(`${fileName} has to be a string`);
}
return cy.fixture(fileName).then((data: unknown) => {
if (!isValidRouteAndPassengerObject(data)) {
throw new Error(`${data} is not valid`);
}
console.log(`this is first`);
this.routeAndPassengerDataObject = data;
return cy.wrap(this._routeAndPassengerDataObject);
});
}
and then calling the object like so-
const routeAndPassengerData = getDataFromRouteAndPassengerObject().then((routeAndPassenger)=>{
console.log(routeAndPassenger)
})

Keeping error information and the outer observable alive

To ensure an error doesn't complete the outer observable, a common rxjs effects pattern I've adopted is:
public saySomething$: Observable<Action> = createEffect(() => {
return this.actions.pipe(
ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
// Switch to the result of the inner observable.
switchMap((action) => {
// This service could fail.
return this.service.saySomething(action.payload).pipe(
// Return `null` to keep the outer observable alive!
catchError((error) => {
// What can I do with error here?
return of(null);
})
)
}),
// The result could be null because something could go wrong.
tap((result: Result | null) => {
if (result) {
// Do something with the result!
}
}),
// Update the store state.
map((result: Result | null) => {
if (result) {
return new AppActions.SaySomethingSuccess(result);
}
// It would be nice if I had access the **error** here.
return new AppActions.SaySomethingFail();
}));
});
Notice that I'm using catchError on the inner observable to keep the outer observable alive if the underlying network call fails (service.saySomething(action.payload)):
catchError((error) => {
// What can I do with error here?
return of(null);
})
The subsequent tap and map operators accommodate this in their signatures by allowing null, i.e. (result: Result | null). However, I lose the error information. Ultimately when the final map method returns new AppActions.SaySomethingFail(); I have lost any information about the error.
How can I keep the error information throughout the pipe rather than losing it at the point it's caught?
As suggested in comments you should use Type guard function
Unfortunately I can't run typescript in snippet so I commented types
const { of, throwError, operators: {
switchMap,
tap,
map,
catchError
}
} = rxjs;
const actions = of({payload: 'data'});
const service = {
saySomething: () => throwError(new Error('test'))
}
const AppActions = {
}
AppActions.SaySomethingSuccess = function () {
}
AppActions.SaySomethingFail = function() {
}
/* Type guard */
function isError(value/*: Result | Error*/)/* value is Error*/ {
return value instanceof Error;
}
const observable = actions.pipe(
switchMap((action) => {
return service.saySomething(action.payload).pipe(
catchError((error) => {
return of(error);
})
)
}),
tap((result/*: Result | Error*/) => {
if (isError(result)) {
console.log('tap error')
return;
}
console.log('tap result');
}),
map((result/*: Result | Error*/) => {
if (isError(result)) {
console.log('map error')
return new AppActions.SaySomethingFail();
}
console.log('map result');
return new AppActions.SaySomethingSuccess(result);
}));
observable.subscribe(_ => {
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>
I wouldn't try to keep the error information throughout the pipe. Instead you should separate your success pipeline (tap, map) from your error pipeline (catchError) by adding all operators to the observable whose result they should actually work with, i.e. your inner observable.
public saySomething$: Observable<Action> = createEffect(() => {
return this.actions.pipe(
ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
switchMap((action) => this.service.saySomething(action.payload).pipe(
tap((result: Result) => {
// Do something with the result!
}),
// Update the store state.
map((result: Result) => {
return new AppActions.SaySomethingSuccess(result);
}),
catchError((error) => {
// I can access the **error** here.
return of(new AppActions.SaySomethingFail());
})
)),
);
});
This way tap and map will only be executed on success results from this.service.saySomething. Move all your error side effects and error mapping into catchError.

AngularFire testing for an object to exist using RxJS Subject

I am trying to test for an object to exist in my AngularFire table. I am having issues returning the subject to detect if the file exists or not.
/**
* Check if the Id exists in storage
* #param Id string | number Key value to check
* #returns Subject<boolean>
*/
public Exists(Id:string):Subject<boolean> {
const Status$:Subject<boolean> = new Subject<boolean>();
let RecordExists:boolean = false;
this.AfDb_.object<T>(`_Testing_/${Id}`).valueChanges()
.subscribe( (OneRecord:T) => {
if (OneRecord.Key_ !== undefined && OneRecord.Key_ !== null && OneRecord.Key_.length > 0) {
RecordExists = true;
}
})
;
Status$.next(RecordExists);
return Status$;
}
This is always returning undefined. My automated tests then fail as well.
it('should confirm a record exists in storage', fakeAsync( () => {
let Exists:boolean;
const Status$:Subject<boolean> = ServiceUnderTest.Exists('Good'); // This exists in Firebase
Status$.subscribe( (Result:boolean) => {
Exists = Result;
});
flushMicrotasks();
Status$.unsubscribe();
expect(Exists).toBeTrue();
}));
I have access in Firebase to /Testing/Good which is an object with a structure of Key_ and Name.
Modules from package.json
"#angular/fire": "^5.4.2",
"firebase": "^7.9.3",
However, if I simply try to return a result without going directly to AngularFire, these tests work.
public Exists(Id:string):BehaviorSubject<boolean> {
const Status:BehaviorSubject<boolean | undefined> = new BehaviorSubject<boolean | undefined>(undefined);
Status.next(true);
return Status;
}
You have to call next on subject when RecordExists came from .valueChanges() e.g.:
let RecordExists:boolean = false;
this.AfDb_.object<T>(`_Testing_/${Id}`).valueChanges()
.subscribe( (OneRecord:T) => {
if (OneRecord.Key_ !== undefined && OneRecord.Key_ !== null && OneRecord.Key_.length > 0) {
Status$.next(true);
} else {
Status$.next(false);
}
})
;
return Status$;
Your code behave in different way in test and simple example because both are call this .valueChanges() in synchronous way so .next() is called after subscribe. In real life valueChanges is async so subscribe is called before next.
====================Edited=====================
to connect with real database you have to modify your test to be async (because connection is async:
it('should confirm a record exists in storage',((done) => {
Status$.subscribe( (Result:boolean) => {
expect(Exists).toBeTrue();
done()
});
d}))

Which RxJS operator to choose to handle HTTP errors: tap or catchError?

/* error handler that will be used below in pipe with catchError()
* when resource fetched with HttpClient get() */
private _handleError<T> (operation: string, result?:T) {
return( error: any): Observable<T> => {
console.error( operation + ' ' + error.message );
// or something else I want to do
return of(result as T); // lets me return innocuous results
}
}
getObjects() {
return this.http.get<any[]>(this.myUrl).pipe(
catchError(this._handleError('my error', [])
);
}
now using tap to handle errors
getObjects() {
return this.http.get<any[]>(this.myUrl).pipe(
tap( objects => {
// whatever action like logging a message for instance
}, err => {
console.error(err);
// whatever else I want to do
})
);
}
Why should I choose one approach instead of the other? Will handling HTTP errors with tap() keep my app' running in case they occur?
tap is to cause side effects.
catchError is to catch errors in a stream and try to handle them.
Therefore if you want to handle errors of http requests use catchError.
http.get('https://test.com/').pipe(
tap({
next: () => {
// 200, awesome!, no errors will trigger it.
},
error: () => {
// error is here, but we can only call side things.
},
}),
catchError(
(error: HttpErrorResponse): Observable<any> => {
// we expect 404, it's not a failure for us.
if (error.status === 404) {
return of(null); // or any other stream like of('') etc.
}
// other errors we don't know how to handle and throw them further.
return throwError(error);
},
),
).subscribe(
response => {
// 200 triggers it with proper response.
// 404 triggers it with null. `tap` can't make 404 valid again.
},
error => {
// any error except 404 will be here.
},
);

Future functions in DART working with ORACLE DART pub

I'm using oracledart pub, and need to get the results returned as Map to the main function, I know it is a FUTURE function, and read about FUTURE, but looks still not clear for me, or I'm doing something wrong in my code, my function is as below:
void main() {
var ORAresults = <Map>[];
ORA()
.then((results) => ORAresults = results)
.catchError((e) => 'Sorry, something wrong!');
}
ORA() {
var results = <Map>[];
connect(
"SYSTEM","pswd",
"(DESCRIPTION="
"(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))"
"(CONNECT_DATA=(SERVICE_NAME=XE)(SERVER=DEDICATED)))")
.then(
(oracleConnection) {
var resultset = oracleConnection.select("select * from vendors");
while(resultset.next()) {
results.add({"code":"vCode 1","name": "${resultset.getStringByName('NAME')}"});
}
print('the results inside $results'); // this works very well
return results;
},
onError: (error) {
print("Failed to connect: $error");
});
}
When I run the above, I get this error:
Breaking on exception: object of type NoSuchMethodError
the file dart:core-patch_object_patch.dart is opening, and pointing to:
noSuchMethod(Invocation invocation) {
=> return _noSuchMethod(invocation.isMethod, // this line is being highlighted!
internal.Symbol.getName(invocation.memberName),
invocation._type,
invocation.positionalArguments,
_symbolMapToStringMap(invocation.namedArguments));
}
I thing the error is due to something wrong here, because if I removed these lines, the error disappear.:
ORA()
.then((results) => ORAresults = results)
.catchError((e) => 'Sorry, something wrong!');
any help pls.
Your ORA() function does not return the Future it uses. Change the connect( line to return connect(, and it should work.
When you do ORA().then(...), you're using ORA()'s return value as a Future, but your ORA() function returns null (it has no return statement, so it returns null by default). What you really want to do is return the Future you're building on with the connect().
Thanks #Tonio and #Robert, I think now I understood the meaning of the FUTURE better :)
I was able to solve the issue, based on your hints and explanations, as below:
in the server.dart
void handlePost(HttpRequest req) {
HttpResponse res = req.response;
switch (req.uri.path) {
...
case '/getVendors':
getVendors(req);
break;
default:
break;
}
}
void getVendors(HttpRequest req) {
HttpResponse res = req.response;
addCorsHeaders(res);
print('${req.method}: ${req.uri.path}');
var vendors = <Map>[];
connect(
"SYSTEM",
"pswrd",
"(DESCRIPTION="
"(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))"
"(CONNECT_DATA=(SERVICE_NAME=XE)(SERVER=DEDICATED)))")
.then(
(oracleConnection) {
var resultset = oracleConnection.select("select * from vendors");
while(resultset.next()) {
vendors.add({"code":"${resultset.getStringByName('CODE')}","name": "${resultset.getStringByName('NAME')}"});
}
res.write(JSON.encode(vendors));
res.close();
},
onError: (error) {
print("Failed to connect: $error");
});
}
and in the client.dart
main(){
HttpRequest request;
String serverResponse = '';
...
}
void submit(){
request = new HttpRequest();
request.onReadyStateChange.listen(onData_getvendors);
var url = 'http://127.0.0.1:8004/getVendors';
request.open('POST', url);
request.send('');
}
onData_getvendors(_){
if (request.readyState == HttpRequest.DONE && request.status == 200) { // Data saved OK.
for(Map vendor in JSON.decode(request.responseText)){
vendor..children.add(new OptionElement(value: vendor['code'], data: vendor['name']));
}
else if (request.readyState == HttpRequest.DONE &&
request.status == 0) { // Status is 0...most likely the server isn't running.
serverResponse=request.responseText;
}
}

Resources