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()
будет вызван, и мы сможем обработать пойманную ошибку и показать ее пользователю.Вместо заключения
Я надеюсь, что для некоторых из вас эта информация оказалась полезной. Обратите внимание, что этой статьей я не пытаюсь заставить вас всегда использовать асинхронные функции — хотя они довольно крутые. У них есть свои варианты использования, но они по-прежнему являются синтаксическим сахаром над промисами.
Я просто хотел, чтобы вы знали, что иногда промисы могут иметь большое значение, и когда (да, не «если») вы столкнетесь с ошибкой, рассмотренной в этой статье, вы будете знать возможную причину ее появления.
P.S. Прим. перев.: к оригинальной статье был оставлен полезный комментарий от пользователя Craig P Hicks, который (после замечаний в комментариях) я решил привести тут:
Хотел бы обратить внимание на одну деталь, (в моей среде разработки) ошибки, которые происходят в телеPromise.resolve({<body>})
не «ловятся»:
Promise.resolve((()=>{throw "oops"; })()) .catch(e=>console("Catched ",e)); // блок .catch() не срабатывает и ошибка не "ловится"
но ошибки, возникающие в телеnew Promise()
(прим. перев.: в оригинале «proper Promise»), «ловятся»:
(new Promise((resolve,reject)=>{ resolve((()=>{throw "oops"})()) })) .catch(e=>console.log("Catched ",e)); // Catched oops
Как насчет этого утверждения:
async function fn() { <body> }
семантически такой вариант эквивалентен этому:
function fn() { return new Promise((resolve,reject)=>{ resolve({ <body> }) }) }
Следовательно, фрагмент кода ниже будет отлавливать ошибки, если в <body> будетnew Promise()
(прим. перев.: в оригинале «proper Promise»):
function fn() { return Promise.resolve({<body}); }
Таким образом, чтобы пример из начала статьи «ловил» ошибки в обоих случаях, нужно возвращать в функциях не
Promise.resolve()
, а new Promise()
: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'
Комментарии (12)
BerkutEagle
11.11.2019 04:28+1Надо просто понимать, во что развернётся сахар async'а.
Перепишите вашу функцию так:
function fn(obj) { return new Promise((resolve, reject) => { try { const someProp = obj.someProp; resolve(someProp); } catch (e) { reject(e); } }) }
mayorovp
11.11.2019 11:02Это как раз пример того как делать НЕ надо. Во-первых, конструктор Promise сам поймает ошибку, try-catch тут полностью лишний. Ну или же лишним можно считать
new Promise
, поскольку try-catch тут и сам справится.
Во-вторых, написать async гораздо проще.
vintage
11.11.2019 13:50Он во что-то такое развернётся:
function fn(obj) { return new Promise( resolve => { const someProp = obj.someProp; resolve(someProp); } ) }
BerkutEagle
11.11.2019 05:23+1Перевод, это конечно круто. Но перевод ради перевода? Если хотели сделать полезное, то перевели бы и единственный комментарий к оригиналу, объясняющий, почему автор неправ.
amakhrov
11.11.2019 09:10+4Иногда встречаются рекомендации начинать цепочку промисов с
Promise.resolve()
— как раз чтобы избежать описанной проблемы.
То есть вместо
userModel.getUserById(req.params.id) .then(user => res.json(user)) .catch()
пишем
Promise.resolve() .then(() => userModel.getUserById(req.params.id)) .then(user => res.json(user)) .catch()
Минус — лишний код. Плюс — работает даже в случае, когда
userModel.getUserById
пришла из сторонней библиотеки, и мы не можем явно пометить ее какasync
justboris
11.11.2019 12:28Выше уже заметили, что нужно помещать код внутрь
new Promise((resolve, reject) => {...})
, чтобы избежать такой проблемы (в случае если async-await по какой-то причине использовать нельзя, например, IE11).
В общем-то именно ради возможности словить ошибку и появилось такое API c замыканием вместо классического
Defferred.resolve/reject
из jQuery и других ранних реализаций промисов. Вот тут можно почитать подробнее: https://stackoverflow.com/questions/28687566/difference-between-defer-promise-and-promiseRifleR
12.11.2019 14:23в случае если async-await по какой-то причине использовать нельзя, например, IE11
ну в IE11 и Promise нативно не поддерживается, так что реальных причин не использовать везде async-await не существует.
justboris
12.11.2019 15:12Полифилл promise подключается один раз и весит 1Kb или около того, а транспиляция раздувает исходный код. Было
async function loadJSON() { const response = await fetch('/url'); const json = await response.json(); return json; }
стало
function loadJSON() { var response, json; return regeneratorRuntime.async(function loadJSON$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return regeneratorRuntime.awrap(fetch('/url')); case 2: response = _context.sent; _context.next = 5; return regeneratorRuntime.awrap(response.json()); case 5: json = _context.sent; return _context.abrupt("return", json); case 7: case "end": return _context.stop(); } } }); }
Поэтому я могу вполне себе представить вариант, где Promise доступен, а async/await – нет.
t_kanstantsin
12.11.2019 15:21Не могу утверждть, работает ли это так как ожидается, но есть плагин для babel, который преобразует async-await в Promise.
Т.е. решая проблему "в лоб" можно глобально подключить полифил + плагин для async->Promisejustboris
12.11.2019 20:19Интересно, хороший плагин, очень жаль, что стандартный
@babel/preset-env
, производит намного больше кода.
mayorovp
13.11.2019 06:48Судя по примеру по ссылке, этот плагин ломает код. При преобразовании
URL.createObjectURL(blob)
в.then(URL.createObjectURL)
у метода createObjectURL теряется контест (this). И если конкретно для этого метода контекст не требуется, для других методов это не так.
megahertz
Для TypeScript полезно правило линтера promise-function-async. Инстинктивно стараюсь избегать вызова асинхронных функций из синхронных, в идеале then/catch остаются только в единственной точке входа.