Ben
Ben

Reputation: 16659

Error handling when getting document from Firestore

In Angular 5 with FireStore and angularfire2, what is the correct way to handle errors when getting a document from a service via a controller?

Service:

getInviteById( inviteId: string ): Promise<any> {    

    // get requested invite from firestore  
    var inviteDocument = this.afs.collection( 'invites' ).doc( inviteId );
    let invite = inviteDocument.ref.get().then( doc => {

        // if document exists
        if (doc.exists) {

            // return id and data
            const id = doc.id; 
            var data = doc.data() as any;
            return { id, ...data };

        // if document does not exist
        } else {
            console.log("Error: No such document!");

            // WHAT DO I NEED TO RETURN HERE???
        }

    // if other error
    }).catch(function(error) {
        console.log("Error: Getting document:", error);                            

        // WHAT DO I NEED TO RETURN HERE???
    });

    // return invite
    return invite;
};

Controller:

this.inviteService.getInviteById( inviteId )
    .then( resolve => {
        this.invite = resolve;
    })
    .catch( err => {
            // THIS NEVER GETS CALLED !
            console.log("INVITE-COMPONENT: Cannot get invite for this id." );
    });

All works well IF a document with the invite-id exists in FireStore. However, if there is no document for the invite id in FireStore, then the service will log "Error: No such document!" (as expected), BUT the component will not go into its own catch case.

How can I process the "no such document" error in my component, so that I can modify my UI accordingly?

Upvotes: 8

Views: 18843

Answers (1)

Roamer-1888
Roamer-1888

Reputation: 19288

You can return a rejected promise but it's simpler to throw.

So, straightforwardly, you might write :

// (1) ILLUSTRATIVE - NOT YET THE FULL SOLUTION
getInviteById(inviteId: string): Promise<any> {
    var inviteDocument = this.afs.collection('invites').doc(inviteId);
    return inviteDocument.ref.get()
    .then(doc => {
        if (doc.exists) { // if document exists ...
            const id = doc.id;
            var data = doc.data() as any;
            return {id, ...data}; // ... return id and data.
        } else { // if document does not exist ...
            throw new Error('No such document!'); // ... throw an Error.
        }
    })
    .catch(error => {
        throw new Error('Error: Getting document:'); // throw an Error
    });
};

HOWEVER, the inner throw would be immediately caught by the outer .catch() and the 'No such document!' error message would be lost in favour of 'Error: Getting document:'.

That loss can be avoided by adjusting the overall pattern as follows:

// (2) ILLUSTRATIVE - NOT YET THE FULL SOLUTION
getInviteById(inviteId: string): Promise<any> {
    var inviteDocument = this.afs.collection('invites').doc(inviteId);
    return inviteDocument.ref.get()
    .catch(error => { // .catch() error arising from inviteDocument.ref.get()
        throw new Error('Error: Getting document:');
    })
    .then(doc => {
        if (doc.exists) {
            const id = doc.id;
            var data = doc.data() as any;
            return {id, ...data};
        } else {
            throw new Error('No such document!'); // can now be caught only by getInviteById's caller
        }
    });
};

HOWEVER, even that isn't yet correct because the possibilities exist that :

  1. this.afs.collection('invites').doc(inviteId) might return null, in which case an error should be thrown.
  2. this.afs.collection('invites').doc(inviteId) or inviteDocument.ref.get() might throw synchronously.

In either case, the caller has a right to expect a promise-returning function always to throw asynchronously regardless of how/where the error arose.

That artifact can be overcome by ensuring var inviteDocument = this.afs.collection('invites').doc(inviteId); and inviteDocument.ref.get() are executed from inside the promise chain and the null case is handled appropriately, as follows :

// (3) SOLUTION
getInviteById(inviteId: string): Promise<any> {
    return Promise.resolve() // neutral starter promise 
    .then(() => {
        var inviteDocument = this.afs.collection('invites').doc(inviteId); // now executed inside promise chain
        if(inviteDocument) {
            return inviteDocument.ref.get(); // now executed inside promise chain.
        } else {
            throw new Error(); // no point setting an error message here as it will be overridden below.
        }
    })
    .catch(error => {
        throw new Error('Error: Getting document:');
    })
    .then(doc => {
        if (doc.exists) {
            const id = doc.id;
            var data = doc.data() as any;
            return {id, ...data};
        } else {
            throw new Error('No such document!');
        }
    });
};

The caller (your controller) will catch and log any error arising from getInviteById() :

this.inviteService.getInviteById(inviteId)
.then(result => { // better not to name the variable `resolve`
    this.invite = result;
})
.catch(err => {
    console.log("INVITE-COMPONENT: Cannot get invite for this id: " + error.message);
});

Notes

  1. console.log() is unnecessary inside getInviteById() (except possibly while debugging). The caller's .catch() will do all the necessary logging.

Upvotes: 5

Related Questions