- Вопрос или проблема
- #1 top-level await в модуле
- #2 – Верхнеуровневая async функция, которая никогда не отказывает
- #3 – then и catch
- Ответ 2023: теперь вы можете использовать верхний уровень await во всех поддерживаемых версиях node.js
- В JavaScript (без использования TypeScript)
- В TypeScript с использованием esrun
- Совместимость
- Ответ или решение
- 1. Понимание async/await
- 2. Основная проблема
- 3. Использование top-level await
- Пример с использованием top-level await
- 4. Использование асинхронной функции
- 5. Обработка ошибок
- 6. Другие методы
- 7. Совместимость
- Заключение
Вопрос или проблема
Я изучал async
/await
и, прочитав несколько статей, решил протестировать это самостоятельно. Однако я не могу понять, почему это не работает:
async function main() {
var value = await Promise.resolve('Привет');
console.log('внутри: ' + value);
return value;
}
var text = main();
console.log('снаружи: ' + text);
Консоль выводит следующее (node v8.6.0):
> снаружи: [object Promise]
> внутри: Привет
Почему лог-сообщение внутри функции выполняется позже? Я думал, что async
/await
был создан для выполнения синхронного выполнения с использованием асинхронных задач.
Есть ли способ, с помощью которого я мог бы использовать значение, возвращаемое внутри функции, без использования .then()
после main()
?
Я не могу понять, почему это не работает.
Потому что main
возвращает промис; все async
функции так делают.
На верхнем уровне вам нужно:
-
Использовать top-level
await
(предложение, MDN; ES2022, широко поддерживается в современных средах), который позволяет использоватьawait
на верхнем уровне в модуле.или
-
Использовать верхнеуровневую
async
функцию, которая никогда не отказывается (если вы не хотите ошибок “необработанного отклонения”).или
-
Использовать
then
иcatch
.
#1 top-level await
в модуле
Вы можете использовать await
на верхнем уровне модуля. Ваш модуль не завершит загрузку, пока промис, который вы await
, не выполнится (это означает, что любой модуль, ожидающий загрузки вашего модуля, не завершит загрузку, пока промис не станет выполненным). Если промис отклоняется, ваш модуль не загрузится. Обычно верхнеуровневый await
используется в ситуациях, когда ваш модуль не сможет выполнить свою работу, пока промис не будет разрешен, и не сможет сделать это вообще, если промис не будет выполнен, так что это нормально:
const text = await main();
console.log(text);
Если ваш модуль может продолжать работать, даже если промис отклоняется, вы могли бы обернуть верхнеуровневый await
в try
/catch
:
// В модуле, как только предложение о верхнем уровне `await` будет принято
try {
const text = await main();
console.log(text);
} catch (e) {
// Обработка факта, что цепочка не удалась
}
// `text` здесь не доступен
Когда модуль, использующий верхнеуровневый await
, оценивается, он возвращает промис загрузчику модулей (как функция async
), который ждет, пока этот промис разрешится, прежде чем оценивать тела любых модулей, которые от него зависят.
Вы не можете использовать await
на верхнем уровне в не модульном скрипте, только в модулях.
#2 – Верхнеуровневая async
функция, которая никогда не отказывает
(async () => {
try {
const text = await main();
console.log(text);
} catch (e) {
// Обработка факта, что цепочка не удалась
}
// `text` здесь тоже не доступен, и код здесь выполняется до того, как промис выполнится
// и до выполнения кода после await
в основной функции выше
Обратите внимание на catch
; вы должны обрабатывать отклонения промисов / асинхронные исключения, так как никто другой этого не сделает; у вас нет вызывающего, которому их можно передать (в отличие от #1 выше, где вашим “вызывающим” является загрузчик модулей). Если хотите, вы можете сделать это на результате вызова через функцию catch
(вместо синтаксиса try
/catch
):
(async () => {
const text = await main();
console.log(text);
})().catch(e => {
// Обработка факта, что цепочка не удалась
});
// `text` здесь не доступен, и код здесь выполняется до того, как промис выполнится
// и до выполнения кода после await
в основной функции выше
…что немного более кратко, хотя это несколько смешивает модели (async
/await
и явные колбеки промисов), что я обычно, в противном случае, советую не делать.
Или, конечно, не обрабатывать ошибки и просто позволить возникнуть ошибке “необработанного отклонения”.
#3 – then
и catch
main()
.then(text => {
console.log(text);
})
.catch(err => {
// Обработка факта, что цепочка не удалась
});
// `text` здесь не доступен, и код здесь выполняется до того, как промис выполнится
// и обработчики выше запускаются
Обработчик catch
будет вызван, если произойдут ошибки в цепочке или в вашем обработчике then
. (Убедитесь, что ваш обработчик catch
не выбрасывает ошибки, так как ничего не зарегистрировано для их обработки.)
Или оба аргумента для then
:
main().then(
text => {
console.log(text);
},
err => {
// Обработка факта, что цепочка не удалась
}
);
// `text` здесь не доступен, и код здесь выполняется до того, как промис выполнится
// и обработчики выше запускаются
Снова обратите внимание, что мы регистрируем обработчик отказов. Но в этой форме, убедитесь, что ни один из ваших колбеков then
не выбрасывает ошибок, так как ничего не зарегистрировано для их обработки.
Ответ 2023: теперь вы можете использовать верхний уровень await во всех поддерживаемых версиях node.js
Большинство ответов выше немного устарели или очень многословны, поэтому вот краткий пример для node 14 и выше.
В JavaScript (без использования TypeScript)
Существует отдельный ответ для TypeScript ниже.
Создайте файл с именем runme.mjs
:
import { exec as oldExec } from "node:child_process";
import { promisify } from "node:util";
const exec = promisify(oldExec);
const log = console.log.bind(console);
// Верхний уровень await теперь работает
const { stdout, stderr } = await exec("ls -la");
log("Вывод:\n", stdout);
log("\n\nОшибки:\n", stderr);
Запустите node runme.mjs
Вывод:
всего 20
drwxr-xr-x 2 mike mike 4096 Авг 12 12:05 .
drwxr-xr-x 30 mike mike 4096 Авг 12 11:05 ..
-rw-r--r-- 1 mike mike 130 Авг 12 12:01 file.json
-rw-r--r-- 1 mike mike 770 Авг 12 12:12 runme.mjs
Ошибки:
Вам не нужно устанавливать type
: module
в ваш package.json
, так как файлы .mjs
предполагаются как ESM.
В TypeScript с использованием esrun
В TypeScript, используя esrun
.
esrun
лучше, чем ts-node
, tsrun
и tsx
, потому что:
- esrun не требует изменений в package.json
- esrun не требует tsconfig.json
- esrun никогда не говорит, что не знает, как запускать ts файлы
- esrun не требует от вас использовать файлы .mts
- esrun по умолчанию использует текущую версию JavaScript
npm i esrun
Просто запустите TS с npx esrun file.ts
:
Вывод:
всего 128
drwxr-xr-x@ 8 mikemaccana staff 256 18 Июл 13:50 .
drwxr-xr-x@ 13 mikemaccana staff 416 17 Июл 14:05 ..
-rw-r--r--@ 1 mikemaccana staff 40 18 Июл 13:27 deleteme.ts
-rw-r--r--@ 1 mikemaccana staff 293 18 Июл 13:51 deletemetoo.ts
drwxr-xr-x@ 85 mikemaccana staff 2720 18 Июл 13:51 node_modules
-rw-r--r--@ 1 mikemaccana staff 46952 18 Июл 13:51 package-lock.json
-rw-r--r--@ 1 mikemaccana staff 503 18 Июл 13:51 package.json
-rw-r--r--@ 1 mikemaccana staff 1788 18 Июл 13:48 utils.ts
⚠️ Вам не нужно устанавливать
type
:module
в вашpackage.json
, так какesrun
представляет собой разумные настройки по умолчанию.
Top-Level await
перешел на этап 3 этап 4 (см. комментарий namo), так что ответ на ваш вопрос Как я могу использовать async/await на верхнем уровне? заключается в том, чтобы просто использовать await
:
const text = await Promise.resolve('Привет');
console.log('снаружи: ' + text)
Или если вы хотите функцию main()
: добавьте await
к вызову main()
:
async function main() {
var value = await Promise.resolve('Привет');
console.log('внутри: ' + value);
return value;
}
var text = await main();
console.log('снаружи: ' + text)
Совместимость
- v8 с октября 2019
- REPL в Chrome DevTools, Node.js и веб-инспектор Safari
- Node v13.3+ с флагом
--harmony-top-level-await
- TypeScript 3.8+ (проблема)
- Deno с октября 2019
- [email protected]
- Bun v1.0.0
Чтобы дать дополнительную информацию в дополнение к текущим ответам:
Содержимое файла node.js
в настоящее время конкатенируется, строкоподобным образом, чтобы сформировать тело функции.
Например, если у вас есть файл test.js
:
// Удивительный тестовый файл!
console.log('Тест!');
Тогда node.js
в тайне скомбинирует функцию, которая выглядит так:
function(require, __dirname /*, ... возможно, больше свойств верхнего уровня ... */) {
// Удивительный тестовый файл!
console.log('Тест!');
}
Главное, что нужно отметить, это то, что полученная функция НЕ является async
функцией. Поэтому вы не можете использовать термин await
непосредственно внутри нее!
Но скажем, вам нужно работать с промисами в этом файле, тогда есть два возможных метода:
- Не использовать
await
непосредственно внутри функции - Совсем не использовать
await
Вариант 1 требует от нас создать новую область видимости (и эта область может быть async
, потому что мы контролируем это):
// Удивительный тестовый файл!
// Создайте новую асинхронную функцию (новую область видимости) и немедленно вызовите ее!
(async () => {
await new Promise(...);
console.log('Тест!');
})()
.catch(err => console.log('Фатальная ошибка', err);
Вариант 2 требует от нас использовать объектно-ориентированный API промисов (менее красивую, но равнофункциональную парадигму работы с промисами)
// Удивительный тестовый файл!
// Создайте какой-то промис...
let myPromise = new Promise(...);
// Теперь используем объектно-ориентированный API
myPromise.then(() => console.log('Тест!'));
Интересно будет увидеть, как node добавляет поддержку верхнего уровня await
!
Теперь вы можете использовать верхний уровень await в Node v13.3.0
import axios from "axios";
const { data } = await axios.get("https://api.namefake.com/");
console.log(data);
запустите с флагом --harmony-top-level-await
node --harmony-top-level-await index.js
Фактическое решение этой проблемы состоит в том, чтобы подойти к ней с другой стороны.
Вероятно, ваша цель – это своего рода инициализация, которая обычно происходит на верхнем уровне приложения.
Решение заключается в том, чтобы убедиться, что на верхнем уровне вашего приложения всегда будет только одно единственное выражение JavaScript. Если у вас есть только одно выражение в верхней части вашего приложения, то вы вольны использовать async/await в любой другой точке повсюду (при обычных правилах синтаксиса)
Иными словами, оберните весь ваш верхний уровень в функцию, чтобы он больше не был верхним уровнем, и это решает вопрос о том, как запускать async/await на верхнем уровне приложения – вы этого не делаете.
Так должен выглядеть верхний уровень вашего приложения:
import {application} from './server'
application();
Мне нравится этот хитроумный синтаксис для выполнения асинхронной работы с точки входа
void async function main() {
await doSomeWork()
await doMoreWork()
}()
Для браузера вам нужно добавить type="module"
без type="module"
<script>
const resp = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await resp.json();
console.log(users)
</script>
с type="module"
<!--script type="module" src="https://stackoverflow.com/questions/79018089/await.js" -->
<script type="module">
const resp = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await resp.json();
console.log(users)
</script>
Другие решения не содержали некоторых важных деталей для соблюдения POSIX:
Вам нужно …
- Сообщать 0 статус выхода при успехе и ненулевой при ошибке.
- Выводить ошибки в поток вывода
stderr
.
Это обеспечит правильную интеграцию вашего командного приложения с другими командными инструментами в bash/zsh и даже Windows PowerShell или cmd. Например, если вы используете его как часть последовательности приложений, связанных через оператор конвейера |
, то вывод ошибок в stderr
обеспечивает их видимость (они не перенаправляются в следующую команду). Только stdout
должен быть перенаправлен как stdin
в следующую команду в вашем конвейере. Возвращение статуса выхода, соответствующего POSIX, означает, что вы можете корректно связывать ваше приложение Node с другими командами, используя оператор &&
или ||
в оболочке.
#!/usr/bin/env node
async function main() {
// ... ожидаем вещи ...
}
// Приложения, соответствующие POSIX, должны сообщать статус выхода
main()
.then(() => {
process.exit(0);
})
.catch(err => {
console.error(err); // Запись в stderr
process.exit(1);
});
Если вы используете парсер командной строки, например commander, вам может не понадобиться main()
.
Пример:
#!/usr/bin/env node
import commander from 'commander'
const program = new commander.Command();
program
.version("0.0.1")
.command("some-cmd")
.arguments("<my-arg1>")
.action(async (arg1: string) => {
// выполнить какое-то асинхронное действие
});
program.parseAsync(process.argv)
.then(() => {
process.exit(0)
})
.catch(err => {
console.error(err.message || err);
if (err.stack) console.error(err.stack);
process.exit(1);
});
Node –
Вы можете запустить node --experimental-repl-await
в REPL. Я не сильно уверен по поводу скриптов.
Deno –
Deno уже имеет это встроенным.
Теперь с ECMAScript22 мы можем использовать await
на верхнем уровне модуля.
Это пример с ( await
на верхнем уровне ):
const response = await fetch("...");
console.log(response):
другой пример без (await
на верхнем уровне )
async function callApi() {
const response = await fetch("...");
console.log(response)
}
callApi()
const resp = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await resp.json();
console.log(users)
-
Вам нужно добавить тип в package.json
"type": "module"
-
Вы готовы начать.
import axios from 'axios'; const res = await axios.get('https://api.github.com/users/wesbos'); console.log(res.data);
Помните, если вы измените тип документа, то вам нужно будет писать код в стиле ES6.
Если ваша единственная цель – контролировать порядок выполнения асинхронного кода, смешанного с другим кодом для тестирования, вы можете обернуть весь верхний уровень кода в немедленно вызываемое функциональное выражение (IIFE), заданное как async
функция. В примере из вопроса, вы затем добавите await
перед вызовом main()
.
Вы можете использовать этот шаблон, когда ваш код еще не находится в async
функции или на верхнем уровне тела модуля. Другими словами, если вы просто тестируете кучу кода внутри js-файла и используете инструменты, такие как Live Server, RunJs или любой другой тип JavaScript-песочницы, чтобы наблюдать за окном консоли, оберните весь ваш код в IIFE, заданное как async
, и используйте ключевое слово await
, когда хотите дождаться завершения асинхронного кода перед выполнением следующей строки.
let topLevelIIFE = (async () => {
async function main() {
var value = await Promise.resolve('Привет');
console.log('внутри: ' + value);
return value;
}
var text = await main();
console.log('снаружи: ' + text);
})()
Вам не нужно использовать этот шаблон, когда вы запускаете код, указанный в теле IIFE, внутри REPL в Chrome DevTools или в другом инструменте REPL браузера, который ведет себя аналогично.
В NodeJS 14.8+ вы можете использовать верхний уровень await модуль (#3 решение). Вы также можете переименовать .js в .mjs (ES модуль) вместо .js (.cjs CommonJS).
const ABC = ()=>{
return new Promise((resolve,reject)=>{
resolve()
})
ABC().then((res)=>{
// сделать что-то
})
}
Поскольку main()
работает асинхронно, он возвращает промис. Вам нужно получить результат в методе then()
. И поскольку then()
также возвращает промис, вам нужно вызвать process.exit()
, чтобы завершить программу.
main()
.then(
(text) => { console.log('снаружи: ' + text) },
(err) => { console.log(err) }
)
.then(() => { process.exit() } )
Ответ или решение
Чтобы использовать async/await
на верхнем уровне (top-level) в JavaScript, вам нужно учитывать несколько основных моментов. Давайте разберем все шаги и примеры, чтобы вы могли использовать их в своих проектах, особенно если вы работаете с Node.js или современными браузерами.
1. Понимание async/await
Когда вы объявляете функцию с ключевым словом async
, она всегда возвращает промис. А когда вы используете await
внутри такой функции, выполнение дожидается разрешения промиса. Ваша начинка кода будет работать асинхронно, но все равно будет выглядеть синхронно в структуре.
2. Основная проблема
Если вы попытаетесь вызвать асинхронную функцию в глобальной области, такая как:
async function main() {
var value = await Promise.resolve('Привет');
console.log('внутри: ' + value);
return value;
}
var text = main();
console.log('снаружи: ' + text);
Консоль выведет:
снаружи: [object Promise]
внутри: Привет
Это происходит потому, что main()
возвращает промис, и console.log('снаружи: ' + text)
выполняется прежде, чем тот промис будет разрешен.
3. Использование top-level await
Если вы используете Node.js версии 14.8 и выше или соответствующий браузер, вы можете использовать top-level await
в модулях. Вот как это выглядит:
Пример с использованием top-level await
// В файле main.mjs
const value = await Promise.resolve('Привет');
console.log('снаружи: ' + value);
Важно, чтобы файл имел расширение .mjs
, чтобы браузер или Node.js распознавал его как модуль. Эта конструкция позволит вам использовать await
вне асинхронной функции.
4. Использование асинхронной функции
Если вы по какой-то причине не можете использовать top-level await
, вы можете обернуть ваш код в асинхронную самовызывающуюся функцию. Например:
(async () => {
const value = await main();
console.log('снаружи: ' + value);
})();
5. Обработка ошибок
При использовании await
на верхнем уровне, необходимо обрабатывать ошибки. В модуле это можно сделать с помощью try/catch
:
try {
const text = await main();
console.log(text);
} catch (error) {
console.error('Ошибка:', error);
}
6. Другие методы
Если вам нужно работать с then
и catch
, вы также можете это сделать:
main()
.then(text => {
console.log('снаружи: ' + text);
})
.catch(error => {
console.error('Ошибка:', error);
});
7. Совместимость
Top-level await
доступен в Node.js начиная с версии 14.8 и в современных браузерах с поддержкой модулей ES, но не работает в скриптах без объявления типа "module".
Заключение
Использование async/await
на верхнем уровне значительно упрощает работу с асинхронным кодом, делая его более читаемым и понятным. Однако важно понимать контекст использования и поддерживаемую версию среды выполнения, в которой вы работаете.