Промисы, асинхронность и ожидание в ReScript (с Bun!)
        ReScript — это «быстрый, простой, полностью типизированный JavaScript из будущего».
Давайте посмотрим, как использовать обещания, асинхронность и ожидание JavaScript в ReScript, используя Bun v1, чтобы быстро запустить и увидеть наши изменения.
ReScript
ReScript — это строго типизированный язык с синтаксисом, подобным JavaScript, который компилируется в JavaScript.
Начало настройки
Мы будем использовать Bun в качестве менеджера пакетов и запускать наш код во время работы.
- Если у вас еще не установлен Bun, запустите 
npm i bun -g. - Создайте новую папку и откройте VSCode или любую другую IDE по вашему выбору.
 - Установите расширение ReScript для вашей IDE.
 - Настройте свой проект с помощью 
bun init. Установите точку входаsrc/index.mjs. - Установите ReScript: 
bun install rescript@next @rescript/core 
Создайте файл bsconfig.json для настройки ReScript:
{
    "name": "bun-rescript",
    "sources": [
        {
            "dir": "src",
            "subdirs": true
        }
    ],
    "package-specs": [
        {
            "module": "es6",
            "in-source": true
        }
    ],
    "suffix": ".mjs",
    "bs-dependencies": [
        "@rescript/core"
    ],
    "bsc-flags": [
        "-open RescriptCore"
    ]
}
Создайте файл src/index.res, который записывает что-то в консоль:
Console.log("starting...")
Запустите bun rescript build -w в одной вкладке или окне терминала и bun --watch src/index.mjs в другой.
Теперь у вас есть ReScript, который быстро компилирует файл .res в файл .mjs за несколько миллисекунд, а затем Bun запускает этот код за несколько миллисекунд. Это очень хороший цикл быстрой обратной связи, который можно использовать для быстрой разработки.
Промисы
Я предполагаю, что у вас есть базовые знания работы с промисами в JavaScript.
Вот действительно простой пример обещания в ReScript:
let main = () => {
  let _ = Promise.resolve(42)->Promise.then(n => Console.log(n)->Promise.resolve)
}
main()
Давайте пройдемся по каждой части кода здесь.
Давайте начнем с понимания того, что происходит с main частью и частью let _ =.
let main = () => {
  let _ = ...
}
main()
В ReScript каждая строка кода является выражением, а последнее выражение в функции — возвращаемым значением.
let fn = () => {
  42 // this function returns 42
}
Примечание. Несмотря на то, что мы не добавляем аннотации типов, ReScript всегда может правильно определить типы, поэтому эта функция имеет сигнатуру типаunit => int.unitв ReScript означает, что у нас вообще нет значения, поэтому эта функция не принимает никаких параметров и возвращаетint.
Любое выражение верхнего уровня должно иметь тип unit в ReScript, а это означает, что мы не можем вернуть что-либо при вызове функции верхнего уровня, как это происходит с main(). Итак, мы должны убедиться, что он возвращает тип unit, и мы можем сделать это, присвоив значение _. _ — это специальный синтаксис для значения, которое существует, но мы никогда не собираемся его использовать. Если бы мы сделали let x = ..., компилятор предупредил бы нас, что x никогда не используется.
Создание обещания выглядит идентично JavaScript:
Promise.resolve(42)
Следующая часть отличается от JS. В ReScript у нас нет цепочки стилей точек, поэтому мы не можем использовать Promise.resolve(42).then(...). В ReScript есть каналы, которые мы используем с оператором ->. Итак, мы берем созданный промис и «перекачиваем» его на следующий шаг — Promise.then.
Promise.resolve(42)->Promise.then(...)
А внутри Promise.then мы выходим на консоль и возвращаем результат (то есть unit) в виде промиса. В ReScript каждый Promise.then должен возвращать еще один промис. Промисы JavaScript творят чудеса, обрабатывая возврат значения или другого промиса внутри .then, и поскольку в ReScript тип может быть только одним, мы должны всегда явно возвращать промис. К счастью, в модуле Promise есть функция thenResolve, которая может это исправить.
let main = () => {
  let _ = Promise.resolve(42)->Promise.thenResolve(n => Console.log(n))
}
main()
Переключение на async/await
Эта функция никогда не может выдать ошибку, поэтому нам не нужно беспокоиться о .catch, поэтому мы можем безопасно преобразовать ее в синтаксис async/await. Если вы хотите избежать ошибок во время выполнения, вам не следует использовать async/await, если только вы не хотите обернуть его в блок try/catch, который может очень быстро стать некрасивым.
В ReScript async/await работает почти так же, как и в JavaScript. Поскольку теперь мы ожидаем, что main() вернет Promise, мы можем удалить часть let _ = ... и заменить ее на await.
let main = async () => {
  await Promise.resolve(42)->Promise.thenResolve(n => Console.log(n))
}
await main()
Давайте заставим его сделать что-нибудь
Вместо того, чтобы возвращать статическое число и регистрировать его, давайте возьмем число и проверим, что оно находится в диапазоне от 0 до 100. Если оно выходит за пределы, мы хотим вернуть его и зарегистрировать ошибку.
let main = async n => {
  await Promise.resolve(n)
  ->Promise.then(n =>
    n >= 1 && n <= 100
      ? Promise.resolve(n)
      : Promise.reject(Exn.raiseError("number is out of bounds"))
  )
  ->Promise.thenResolve(n => Console.log(n->Int.toString ++ " is a valid number!"))
}
await main(10)
Мы должны увидеть, что 10 is a valid number! в консоли Bun, но мы не обработали ошибку должным образом, если присвоили ей неверный номер, и получили исключение во время выполнения.
4 | function raiseError(str) {
5 |   throw new Error(str);
            ^
error: number is out of bounds
      at raiseError
Мы можем улучшить это, используя тип Result ReScript, который представляет собой вариант типа «Ok» или «Error».
let main = async n => {
  await Promise.resolve(n)
  ->Promise.thenResolve(n =>
    n >= 1 && n <= 100
      ? Ok(n->Int.toString ++ " is a valid number!")
      : Error(n->Int.toString ++ " is out of bounds")
  )
  ->Promise.thenResolve(res =>
    switch res {
    | Ok(message) => Console.log(message)
    | Error(err) => Console.error(err)
    }
  )
}
await main(1000) // => 1000 is out of bounds
await main(10) // => 10 is a valid number!
Подведение итогов
Теперь у вас должно быть базовое представление о том, как использовать обещания в ReScript. Ключевой момент, на мой взгляд, заключается в том, что нам не нужно выдавать ошибки в наших обещаниях, поскольку у нас есть тип Result. Для разработчиков и пользователей удобнее фиксировать известные ошибки и корректно их обрабатывать, возвращая их в ответе API или отображая ошибку пользователю в приложении React.
Конечно, случаются неизвестные исключения, но в этом случае мы ожидаем, что номер может быть недействительным. Что, если наша функция была определена здесь и предназначена для использования где-то еще? Давайте перепишем его так, чтобы он возвращал либо число, либо сообщение об ошибке.
let validateQuantity = async n => {
  await Promise.resolve(n)->Promise.thenResolve(n =>
    n >= 1 && n <= 100
      ? Ok(n)
      : Error(n->Int.toString ++ " is out of bounds and is not a valid quantity.")
  )
}
Теперь функция вернет promise<result<int, string>>, поэтому любой, кто ее использует, знает, что мы ожидаем случай ошибки и можем обработать его соответствующим образом.
Мы даже можем сделать сообщения об ошибках более значимыми, если изменим это на использование сопоставления с образцом:
let validateQuantity = async n => {
  await Promise.resolve(n)->Promise.thenResolve(n =>
    switch [n >= 1, n <= 100] {
    | [false, _] => Error(n->Int.toString ++ " is less than 0 and is not a valid quantity.")
    | [_, false] => Error(n->Int.toString ++ " is greater than 100 and is not a valid quantity.")
    | _ => Ok(n)
    }
  )
}
Это позволит нам показывать пользователю значимое сообщение об ошибке, если он попытается сделать что-то недопустимое.
let validateQuantity = async n => {
  await Promise.resolve(n)->Promise.thenResolve(n =>
    switch [n >= 1, n <= 100] {
    | [false, _] => Error(n->Int.toString ++ " is less than 1 and is not a valid quantity.")
    | [_, false] => Error(n->Int.toString ++ " is greater than 100 and is not a valid quantity.")
    | _ => Ok(n)
    }
  )
}
let addToCart = async quantity => {
  let validatedQuantity = await validateQuantity(quantity)
  switch validatedQuantity {
  | Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
  | Error(e) => Console.error(e)
  }
}
await addToCart(10) // => 10 items successfully added to cart!
await addToCart(1000) // => 1000 is greater than 100 and is not a valid quantity.
await addToCart(0) // => 0 is less than 1 and is not a valid quantity.
ДОПОЛНЕНИЕ от Габриэля Нордеборна, далее следует текст его комментария
Я хотел бы добавить две вещи, которые я считаю крутыми в отношении async/await в ReScript:
- Вы можете ждать прямо в коммутаторе. Итак, вы могли бы, например, написать 
addToCartтак: 
let addToCart = async quantity => {
  switch await validateQuantity(quantity) {
  | Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
  | Error(e) => Console.error(e)
  }
}
- Точно так же вы упомянули, что асинхронные действия, которые могут выдать ошибку, требуют набора блоков 
try-catch, которые часто засоряют код. Но в ReScript есть хитрая хитрость, позволяющая упростить это и внутри нашего любимого переключателя. На самом деле вы можете сопоставить шаблон с исключением и обработать это исключение прямо в своем коммутаторе. Представьте, чтоvalidateQuantityможет выбросить. Вы могли бы написать это так: 
let addToCart = async quantity => {
  switch await validateQuantity(quantity) {
  | Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
  | Error(e) => Console.error(e)
  | exception Exn.Error(err) => Console.error(err)
  }
}
Это скомпилирует в JS try/catch, которая обработает любую асинхронную ошибку и в то же время позволит вашему коду ReScript оставаться действительно кратким.