Существует небольшая, но довольно важная разница между функцией, которая просто возвращает промис, и функцией, которая была объявлена с помощью ключевого слова async
.
Взгляните на следующий фрагмент кода:
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
Как видите, обе функции имеют одно и то же тело, в котором мы пытаемся получить доступ к свойству аргумента, который не определен в обоих случаях. Единственное различие между этими двумя функциями заключается в том, что asyncFn
объявляется с помощью ключевого слова async
.
Это значит, что JavaScript гарантирует, что функция asnycFn
вернет промис (либо выполнится успешно, либо выполнится с ошибкой), даже если в нем произошла ошибка, в нашем случае блок .catch()
поймает ее.
Однако в случае с функцией fn
движок еще не знает, что функция вернет промис, и поэтому выполнение кода не дойдет до блока .catch()
, ошибка не будет поймана и вывалится в консоль.
Более жизненный пример
Я знаю, о чем вы сейчас думаете:
«Когда же, черт возьми, я совершу такую ошибку?»
Угадал?
Ну, давайте создадим простое приложение, которое делает именно это.
Допустим, у нас есть приложение, созданное с помощью Express и MongoDB, использующее драйвер MongoDB Node.JS. Если вы мне не доверяете, я разместил весь исходный код в этом репозитории Github, поэтому вы можете клонировать его и запустить локально, но я также продублирую весь код здесь.
Вот наш файл app.js
:
// 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'))
Внимательно посмотрите на блок .catch()
! Вот где будет (не будет) происходить магия.
Файл db.js
используется для подключения к базе данных mongo:
'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
}
}
И, наконец, у нас есть файл user-model.js
, в котором на данный момент определена только одна функция 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) })
}
}
Если вы снова посмотрите на файл app.js
, вы увидите, что при переходе по адресу localhost:3000/users/<id>
мы вызываем функцию getUserById
, определенную в файле user-model.js
, передав в качестве запроса параметр id
.
Допустим, вы переходите по следующему адресу: localhost:3000/users/1
. Как думаете, что произойдет дальше?
Ну, если вы ответили: «Я увижу огромную ошибку от MongoClient» — вы были правы. Чтобы быть точнее, вы увидите следующую ошибку: Error: Argument passed in must be a single String of 12 bytes or a string of 24 hex characters
.
И как вы думаете, будет ли вызван блок .catch()
в следующем фрагменте кода?
// 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' })) // <=== ВОТ ЭТОТ!
})
// ... код ...
Нет. Он не будет вызван.
А что произойдет, если вы измените объявление функции на это?
module.exports = {
// Обратите внимание, что ключевое слово async должно быть именно тут!
async findById(id) {
return db
.getDb()
.collection(collectionName)
.findOne({ _id: new ObjectId(id) })
}
}
Ага, вы начинаете понимать, что к чему. Наш блок .catch()
будет вызван, и мы сможем обработать пойманную ошибку и показать ее пользователю.
Вместо заключения
Я надеюсь, что для некоторых из вас эта информация оказалась полезной. Обратите внимание, что этой статьей я не пытаюсь заставить вас всегда использовать асинхронные функции — хотя они довольно крутые. У них есть свои варианты использования, но они по-прежнему являются синтаксическим сахаром над промисами.
Я просто хотел, чтобы вы знали, что иногда промисы могут иметь большое значение, и когда (да, не «если») вы столкнетесь с ошибкой, рассмотренной в этой статье, вы будете знать возможную причину ее появления.
Автор: Ruslan Gaiazov