[Перевод] Разница между асинхронной функцией и функцией, возвращающей промис

?v=1

Существует небольшая, но довольно важная разница между функцией, которая просто возвращает промис, и функцией, которая была объявлена с помощью ключевого слова 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} The user object
   */
  getUserById(id) {
    return db
      .getDb()
      .collection(collectionName)
      .findOne({ _id: new ObjectId(id) })
  }
}



Если вы снова посмотрите на файл app.js, вы увидите, что при переходе по адресу localhost:3000/users/ мы вызываем функцию 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() будет вызван, и мы сможем обработать пойманную ошибку и показать ее пользователю.

Вместо заключения


Я надеюсь, что для некоторых из вас эта информация оказалась полезной. Обратите внимание, что этой статьей я не пытаюсь заставить вас всегда использовать асинхронные функции — хотя они довольно крутые. У них есть свои варианты использования, но они по-прежнему являются синтаксическим сахаром над промисами.

Я просто хотел, чтобы вы знали, что иногда промисы могут иметь большое значение, и когда (да, не «если») вы столкнетесь с ошибкой, рассмотренной в этой статье, вы будете знать возможную причину ее появления.

© Habrahabr.ru