Onfire
Onfire

Reputation: 364

Best way to handle Promise catch errors when I sometimes want it to fail silently

I am writing a Ionic app using Ionic 4 and I am having some trouble getting my Promises to execute in the correct order (or perhaps I am just thinking about this wrong). It's my first time using Typescript as well, so bear with me.

The app needs to interface with our API which uses Oauth. I am storing the Oauth tokens using ionic storage, which also uses Promises for get/set so this is adding to my problem.

If you take the following file snippets:

oauth.service.ts:

export class OauthService {

...    

    public async setTokens(token: string, token_secret: string) {
        return Promise.all([this.storage.set('token', token), this.storage.set('token_secret', token_secret)]);
    }

    public async getTokens() {
        return Promise.all([this.storage.get('token'), this.storage.get('token_secret')]);
    }

...

}

api.service.ts:

export class ApiService {

...

    public async getCustomer() {
        const requestData = {
            .. request data ..
        };

        return this.authorisedRequest(requestData);
    }

    private authorisedRequest(requestData) {
        return this.oauth.getTokens().then(([token, token_secret]) => {

            if (!token || !token_secret) {
                return Promise.reject('Tokens not available');
            }

            const tokens = {
                'key': token,
                'secret': token_secret
            };

            const oauthHeader = this.oauth.createHeader(requestData, tokens);
            const headers = this.createHeaders({
                'Authorization': oauthHeader.Authorization
            });

            return this.apiRequest(requestData.method, requestData.url, {}, headers);

        }).catch((error) => {
            // @todo what to do here, if anything?
            console.info('token error:', error)
        });
    }

    private async apiRequest(type, path, data, headers = null) {
        if (!headers) {
            headers = this.headers;
        }

        const response = new Subject();
        const httpRequest = new HttpRequest(
            type,
            path,
            data,
            {
                headers: headers
            }
        );

        this.http.request(httpRequest).subscribe((res: any) => {
            if (res.type) {
                response.next(res.body);
            }
        }, error => {
            const responseError = error.error.messages.error[0];
            this.alerter.presentAlert(responseError.message);

            response.error(error);
        });

        return response;
    }

}

authentication.service.ts:

export class AuthenticationService {

...

    public checkAuth() {
        this.api.getCustomer().then((request: Subject<any>) => {

           // this still executes but request is undefined.

            request.subscribe((resp: any) => {
                this.isLoggedIn = true;
            }, (error) => {
                this.isLoggedIn = false;
            });
        });
    }

...

}

For the most part this is working ok, in all cases where the token does exist as the promise is not rejected.

However when I run checkAuth() on init (to check if the user is already logged in) the getTokens() promise returns a reject which is caught straight away (in api.service) but the 'then' inside checkAuth is still run even though it should have been caught, which gives me an error:

TypeError: Cannot read property 'subscribe' of undefined

I can move the catch block to inside the checkAuth function, but that would mean I have to do it in all cases where I am doing an API call (~30 odd endpoints) which is not ideal.

With no catch at all I get this error:

Uncaught (in promise): Tokens not available

How can I either have the reject fail silently, or perhaps just pass the error along checkAuth?

Or am I going about this process the wrong way entirely? I do have a feeling that my process for retrieving the oauth token is wrong here (causing the nested promises for any api call).

Upvotes: 1

Views: 639

Answers (1)

mykhailo.romaniuk
mykhailo.romaniuk

Reputation: 1088

The main problem is that you are mixing Observables with Promises in the wrong way.

For simplicity, I suggest using only one of them at a time.

Simple solution:

checkAuth() {
   this.api.getCustomer()
       .then((request: Subject<any>) => request.toPromise())
       .then(() => { this.isLoggedIn = true; })
       .catch(() => { this.isLoggedIn = false; });
}

or

import { from } from 'rxjs';

checkAuth() {
   const customersObservable = from(this.api.getCustomer());
   customersObservable.subscribe(
       () => { this.isLoggedIn = true; },
       () => { this.isLoggedIn = false; }
   );
}

Better solution:

Use Promises or Observables on a low level to make your service's API clear.

Example with converting Observables into Promises:

export class OauthService {
    public async getTokens(): Promise<any> { ... }
}

export class ApiService {
    public async getCustomers(): Promise<Customer> {
        ...
        return await this.authRequest(someRequest);
    }

    private async authorisedRequest(request) : Promise<any> {
        const [token, token_secret] = await this.oauth.getTokens();

        if (!token || !token_secret) {
            throw 'Tokens not available';
        }

        return await this.apiRequest(request);
    }

    private async apiRequest(request) : Promise<any> {
        const httpRequest = ...;
        // Here we are converting our Observable to a Promise to avoid mixing
        return await this.http.request(httpRequest)
            .toPromise();
    }
}

export class AuthenticationService {
    public async checkAuth() {
        try {
            await this.api.getCustomer();
            this.isLoggedIn = true;
        } catch {
            this.isLoggedIn = false;
        }
    }
}

You also can use an approach with Observable by converting promise to observable (in general code will be similar to the example with promises, so I skipping it)

Upvotes: 1

Related Questions