The difference between an asynchronous function and a function that returns a promise

There is a small but rather important difference between a function that simply returns a promise and a function that was declared using the async



.



Take a look at the following code snippet:



 function fn(obj) { const someProp = obj.someProp return Promise.resolve(someProp) } async function asyncFn(obj) { const someProp = obj.someProp return Promise.resolve(someProp) } asyncFn().catch(err => console.error('Catched')) // => 'Catched' fn().catch(err => console.error('Catched')) // => TypeError: Cannot read property 'someProp' of undefined
      
      





As you can see, both functions have the same body in which we are trying to access the property of an argument that is not defined in both cases. The only difference between the two functions is that asyncFn



declared using the async



.



This means that JavaScript ensures that the asnycFn



function returns a promise (either succeeds or fails), even if an error occurs in it, in our case the .catch()



block will catch it.



However, in the case of the fn



function, the engine does not yet know that the function will return a promise, and therefore the code will not reach the .catch()



block, the error will not be caught and will fall out to the console.



More life example



I know what you are thinking now:

“When the hell will I make such a mistake?”


Guessed?



Well, let's create a simple application that does just that.



Suppose we have an application built using Express and MongoDB that uses the MongoDB Node.JS driver. If you don’t trust me, I have placed all the source code in this Github repository , so you can clone it and run it locally, but I will also duplicate all the code here.



Here is our app.js



file:



 // app.js 'use strict' const express = require('express') const db = require('./db') const userModel = require('./models/user-model') const app = express() db.connect() app.get('/users/:id', (req, res) => { return userModel .getUserById(req.params.id) .then(user => res.json(user)) .catch(err => res.status(400).json({ error: 'An error occured' })) // <===  ! }) app.listen(3000, () => console.log('Server is listening'))
      
      





Take a close look at the .catch()



block! This is where magic will (will not) happen.



The db.js



file db.js



used to connect to the mongo database:



 'use strict' const MongoClient = require('mongodb').MongoClient const url = 'mongodb://localhost:27017' const dbName = 'async-promise-test' const client = new MongoClient(url) let db module.exports = { connect() { return new Promise((resolve, reject) => { client.connect(err => { if (err) return reject(err) console.log('Connected successfully to server') db = client.db(dbName) resolve(db) }) }) }, getDb() { return db } }
      
      





And finally, we have a user-model.js



file in which only one getUserById



function is currently defined:



 // models/user-model.js 'use strict' const ObjectId = require('mongodb').ObjectId const db = require('../db') const collectionName = 'users' module.exports = { /** * Get's a user by it's ID * @param {string} id The id of the user * @returns {Promise<Object>} The user object */ getUserById(id) { return db .getDb() .collection(collectionName) .findOne({ _id: new ObjectId(id) }) } }
      
      





If you look at the app.js



file app.js



, you will see that when we go to localhost:3000/users/<id>



we call the getUserById



function defined in the user-model.js



, passing the id



parameter as the request.



Suppose you go to the following address: localhost:3000/users/1



. What do you think will happen next?



Well, if you answered: “I will see a huge mistake from MongoClient” - you were right. To be more precise, you will see the following error: Error: Argument passed in must be a single String of 12 bytes or a string of 24 hex characters



.



And do you think the .catch()



block will be called in the following code fragment?



 // app.js // ...  ... app.get('/users/:id', (req, res) => { return userModel .getUserById(req.params.id) .then(user => res.json(user)) .catch(err => res.status(400).json({ error: 'An error occured' })) // <===  ! }) // ...  ...
      
      





Not. He will not be called.



And what happens if you change a function declaration to this?



 module.exports = { //  ,    async    ! async findById(id) { return db .getDb() .collection(collectionName) .findOne({ _id: new ObjectId(id) }) } }
      
      





Yeah, you begin to understand what's what. Our .catch()



block will be called, and we will be able to process the caught error and show it to the user.



Instead of a conclusion



I hope this information has been helpful to some of you. Please note that this article is not trying to force you to always use asynchronous functions - although they are pretty cool. They have their own use cases, but they are still syntactic sugar over promises.



I just wanted you to know that sometimes promises can make a big difference, and when (yes, not “if”) you encounter the error discussed in this article, you will know the possible reason for its occurrence.



PS Note perev .: to the original article, a useful comment was left from the user Craig P Hicks, which (after comments in the comments) I decided to quote here:

I would like to draw attention to one detail, (in my development environment) errors that occur in the body of Promise.resolve({<body>})



not "caught":



 Promise.resolve((()=>{throw "oops"; })()) .catch(e=>console("Catched ",e)); //  .catch()      ""
      
      





but errors that occur in the body of new Promise()



( approx. transl .: in the original “proper Promise” ) are “caught”:



 (new Promise((resolve,reject)=>{ resolve((()=>{throw "oops"})()) })) .catch(e=>console.log("Catched ",e)); // Catched oops
      
      





How about this statement:



 async function fn() { <body> }
      
      





semantically, this option is equivalent to this:



 function fn() { return new Promise((resolve,reject)=>{ resolve({ <body> }) }) }
      
      





Therefore, the code snippet below will catch errors if the <body> has new Promise()



( approx. Transl .: in the original “proper Promise” ):



 function fn() { return Promise.resolve({<body}); }
      
      





Thus, in order for the example from the beginning of the article to “catch” errors in both cases, it is necessary to return not Promise.resolve()



, but new Promise()



in the functions:




 function fn(obj) { return new Promise((resolve, reject) => { const someProp = obj.someProp; resolve(someProp); }); } async function asyncFn(obj) { return new Promise((resolve, reject) => { const someProp = obj.someProp; resolve(someProp); }); } asyncFn().catch(err => console.error("Catched")); // => 'Catched' fn().catch(err => console.error("Catched")); // => 'Catched'
      
      






All Articles