Async function vs. a function that returns a Promise
Mayer János
Posted on October 16, 2018
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.
Posted on October 16, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.