DEV Community

Mayer János
Mayer János

Posted on

Async function vs. a function that returns a Promise

There is a small, but quite important difference between a function that just returns a Promise, and a function that was declared with the async keyword.

Take a look at the following 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 of the functions above have the same body in which we try to access a property of an argument that is undefined in both cases. The only difference between the two functions is that asyncFn is declared with the async keyword.

This means that Javascript will make sure that the asnycFn will return with a Promise (either resolved or rejected) even if an error occured in it, in our case calling our .catch() block.

However with the fn function the engine doesn't yet know that the function will return a Promise and thus it will not call our catch() block.

A more real-world version

I know what you are thinking right now:

"When the heck will I ever make such a mistake?"

Right?

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

Let's say we have an express app with MongoDB using MongoDB's Node.JS driver. If you don't trust me I have put all the code on this github repo, so you can clone and run it locally, but I will also copy-paste all the code here, too.

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 good look at that .catch block in the route definition! That's where the magic will (well not) happen.

The db.js file can be used to connect to the mongo database and get the db connection:

'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 the user model file, which for now only has one function called getUserById:

// 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 back at the app.js file you can see that upon visiting the site at the url localhost:3000/users/<id> we would call the getUserById function defined in the user-model file, passing in the id parameter of the request.

Let's say you visit the following url: localhost:3000/users/1. What do you think what will happen?

Well if you answered: "I will get a huge error from the mongo client" - you were right. To be exact you will get an error like this:

Error: Argument passed in must be a single String of 12 bytes or a string of 24 hex characters

And what do you think, will this (emphasized via a comment) .catch block be called?

// app.js

// ... stuff ...

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' })) // <=== THIS ONE HERE!
})

// ... stuff ...

Nope.

Not by the slightest.

And what would happen if you'd change the function declaration to this?

module.exports = {
  // Note that async keyword right there!
  async findById(id) {
    return db
      .getDb()
      .collection(collectionName)
      .findOne({ _id: new ObjectId(id) })
  }
}

Yep, you're getting the hang of it. Our .catch() block would be called and we would respond to the user with a nice json error.

Parting thoughts

I hope that for some of you this information was new(ish). Note however, that with this post I'm not trying to get you to always use an async function - though they are pretty freakin' awesome. They have their use cases, but they are still just syntactic sugar over the Promises.

I simply wanted you to know, that sometimes being a bit extra careful these Promises can go a long way and when (yeah, not 'if') you will have an error like the one above, you may know where the problem comes from.

Top comments (1)

Collapse
 
craigphicks profile image
Craig P Hicks

Just to report a detail, (in my environment) errors that occur within the body of Promise.resolve({<body>}) are not "Catched":

Promise.resolve((()=>{throw "oops"; })())
    .catch(e=>console("Catched ",e));
// escapes, "Error: oops" reported further up 

but error occurring in the body of a proper Promise are "Catched":

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

How about this assertion:

async function fn() { <body> }

is semantically equivalent to

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

Corrollary:
The following is only a Proper Promise if <body> is a proper Promise:

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