June 3

Предложение: try/catch как выражение (с ключевым словом raise)

Введение

Данное предложение предлагает новую возможность в JavaScript: использование конструкции try/catch/finally в контексте выражения, возвращающего значение. Для этого вводится новое ключевое слово raise, предназначенное для явного "выноса" значения из блока try или catch наружу. В итоге весь блок try/catch может выступать как единое выражение, результат которого определяется вызовами raise внутри него.

В классическом JavaScript конструкция try...catch является оператором и не напрямую возвращает значение. Это означает, что для использования результата выполнения try/catch в выражении приходится прибегать к дополнительным переменным или функциям. Новое же предложение делает синтаксис более выразительным и функциональным, позволяя писать код в декларативном стиле без лишних промежуточных шагов.

Ниже рассматриваются мотивация данного предложения, примеры использования, синтаксис и семантика ключевого слова raise, потенциальные сложности и влияние на существующий код, а также сравнение с похожими возможностями в других языках. Предложение оформлено в формате объяснительного документа (explainer) для нулевого этапа TC39.

Мотивация

Основная мотивация – упростить и улучшить читаемость кода, связанного с обработкой ошибок, особенно в случаях, когда нужно присвоить переменной значение, полученное либо при успешном выполнении операции, либо при отлове исключения. В текущем JavaScript подобный паттерн требует многословного императивного кода:

let result;
try {
  result = computeValue(x);
} catch (e) {
  result = fallbackValue(e);
}

В таком коде переменная result объявляется заранее, а затем внутри try/catch ей присваиваются значения. Этот шаблон нарушает декларативность и поток чтения кода – вместо того, чтобы определить result в месте вычисления, мы вынуждены сначала объявить ее, а потом изменять. Желательно, чтобы можно было получить значение из try/catch прямо в месте вызова, как это делается с тернарными операторами или if/else (если бы if мог быть выражением).

Другой пример неудобства – использование результатов внутри других выражений. Сегодня, чтобы, например, вызвать функцию с аргументом, полученным через try/catch, приходится либо выносить вычисление вне вызова, либо использовать Immediately-Invoked Function Expression (IIFE):

// Текущий подход без try-выражения:
const finalResult = processData(
  (() => {
    try {
      return riskyOperation();
    } catch {
      return defaultValue;
    }
  })()
);

Такой код труднее читать и поддерживать. Если бы try/catch был выражением, можно было бы писать гораздо яснее:

// С предлагаемым try-выражением:
const finalResult = processData(
  try {
    raise riskyOperation();
  } catch (e) {
    raise defaultValue;
  }
);

Здесь результат try-выражения сразу передается в функцию processData, без дополнительных функций-обёрток. Код выглядит лаконичнее и очевиднее.

Помимо присваивания, выражение try вписывается в идеологию более функционального стиля: можно использовать его внутри других выражений, комбинировать с тернарными операторами, &&/|| логикой, шаблонными литералами и т.д. Это особенно ценно при сложных условиях, когда решение об значении переменной зависит как от булевых условий, так и от успешности выполнения какого-то кода.

Обработка ошибок становится частью выражения, а не отдельным "побочным эффектом" со стороны. Это может поощрять более консистентный стиль кодирования, где минимум изменяемых переменных и промежуточных шагов.

Наконец, стоит отметить интерес сообщества к подобным возможностям. В других языках и предложениях уже есть схожие решения. Например, сообщество Python обсуждало возможность так называемых "exception-catching expressions" – выражений с обработкой исключений встроенной, по аналогии с тернарными операторами peps.python.org. Хотя в Python это не было принято, сам факт появления PEP 463 показывает потребность разработчиков в более кратком синтаксисе для таких случаев. Java добавила в версии 13 выражения switch с ключевым словом yield для возврата значений из блоков case, чтобы избавиться от шаблона с внешней переменной stackoverflow.com. Наше предложение в духе этой тенденции – предоставить JavaScript-разработчикам инструмент, делающий код с try/catch менее шаблонным и более выразительным.

Примеры использования до и после

Чтобы лучше понять предлагаемое изменение, рассмотрим несколько пар примеров "до и после". Они иллюстрируют, как новый синтаксис с raise сократит код и улучшит его понятность.

Присваивание с обработкой ошибок:

До:

let config;
try {
  config = loadConfig();
} catch {
  config = getDefaultConfig();
}

После:

const config = try {
  raise loadConfig();
} catch {
  raise getDefaultConfig();
};

Теперь переменная config объявляется как const и сразу инициализируется результатом try-выражения. В случае успеха вызова loadConfig() значение "поднимается" наружу через raise внутри блока try. Если же случится исключение, блок catch выполнит свой raise с результатом getDefaultConfig(). В итоге config получит либо загруженную конфигурацию, либо конфигурацию по умолчанию – и все это в одном выразительном конструктиве.

Интеграция с условными операторами (if/тернарный):

До:

let userName;
if (user.isLoggedIn) {
  try {
    userName = fetchUserName(user.id);
  } catch {
    userName = "Гость";
  }
} else {
  userName = "Не авторизован";
}

Этот код можно переписать с использованием выражений, вложив try внутрь тернарного оператора.

После:

const userName = user.isLoggedIn 
  ? try {
      raise fetchUserName(user.id);
    } catch {
      raise "Гость";
    }
  : "Не авторизован";

Логика осталась прежней, но запись стала короче. Вместо того чтобы объявлять userName заранее и несколько раз присваивать, мы напрямую определяем ее через выражение. При залогиненом пользователе предпринимается попытка получить имя (в случае ошибки подставится "Гость"), а при отсутствующей авторизации сразу используется строка "Не авторизован". Код легко читается как единое выражение, описывающее правила вычисления значения.

Использование в async функциях с await:

Нововведение совместимо с асинхронным кодом. Ключевое слово raise поддерживает await для правильного получения промиса.

До:

async function getData() {
  try {
    const response = await fetch(url);
    return process(response);
  } catch (err) {
    return getFallbackData(err);
  }
}

После:

async function getData() {
  return try {
    raise await fetch(url);
  } catch (err) {
    raise getFallbackData(err);
  };
}

Здесь мы сразу return-им результат try-выражения из функции. Внутри try происходит await fetch(url), и полученный ответ подается в raise – то есть либо возвращается как результат всей функции (если все прошло успешно), либо, при ошибке запроса, управление перейдет в блок catch, который вызовет raise getFallbackData(err) и вернет альтернативные данные. Обратите внимание: raise await аналогичен комбинации await + return для промиса – он дожидается результата и "поднимает" его наружу.

Эти примеры демонстрируют, как новая возможность устраняет шаблонное кодирование (boilerplate) и делает намерения разработчика более прозрачными. Вместо фрагментированного кода с объявлениями вне блоков и присвоениями внутри, мы получаем целостные выражения, которые проще воспринимать.

Синтаксис и семантика try-выражения с raise

Предлагаемое изменение включает два связанных элемента: разрешить конструкции try/catch/finally возвращать значение, и ввести ключевое слово raise для явного указания возвращаемого значения. Ниже описаны основные правила и поведение.

Основные правила и ограничения

Обязательность raise в ветвях:

Если хотя бы одна из ветвей (try или catch) внутри конструкции использует raise для возврата значения, то обе ветви должны содержать raise. Другими словами, в выражении вида try { ... } catch { ... } не допускается ситуация, когда try возвращает значение через raise, а в catch при этом нет raise (или наоборот). Такое требование гарантирует, что все возможные пути выполнения возвращают значение. Это схоже с тем, как в выражениях switch в Java каждой ветке должно соответствовать возвращаемое значение (через -> или yield), иначе код считается неполным docs.oracle.comdocs.oracle.com. Если ни одна из ветвей не содержит raise, конструкция try/catch работает как обычный оператор и не производит значения (то есть не может использоваться там, где ожидается значение).

Возврат значения из блока try/catch:

Ключевое слово raise действует по аналогии с return, но не на уровне функции, а на уровне самого блока try/catch. Когда выполнение кода встречает raise <выражение>, текущий блок (try или catch) немедленно завершается, и указанное значение "поднимается" наружу, становясь результатом всего выражения try/catch. Код после вызова raise в том же блоке не выполняется (как и в случае с return внутри функции). Например:

const value = try {
  if (conditionFailed) {
    raise null;  // немедленный выход из try-блока с результатом null
  }
  // ... иначе продолжаем вычисления
  let result = compute();
  raise result;  // возвращаем вычисленное значение
  console.log("Этот код уже не исполнится, т.к. выше был raise");
} catch (e) {
  raise handleError(e);
};

Здесь внутри try предусмотрен ранний выход: при нарушении условия сразу возвращается null. Если же условие в порядке, вычисляется result и он возвращается. Любой код после raise не выполнится. Блок catch также обязательно возвращает значение (обработку ошибки) через raise. Таким образом, переменная value получит либо null, либо корректно вычисленный результат, либо (если исключение не перехвачено данным catch) произойдет обычное распространение исключения вверх (см. ниже).

Поведение блока catch:

Блок catch с raise срабатывает только если в блоке try было брошено исключение (с помощью обычного throw или из-за ошибки). В этом случае выполнение переходит в catch, и там зачастую пишется raise с каким-то значением-заменой или обработкой. Если в catch есть raise, как требует правило, то при перехвате исключения именно его значение станет результатом всего try-выражения. Если же исключение не произошо в try, то блок catch пропускается (как обычно) и не влияет на результат.

Опциональный блок finally:

Конструкция может включать блок finally так же, как обычный try/catch/finally. Блок finally выполняется всегда после try/catch – вне зависимости, было исключение или нет. Однако, в контексте выражения важно, как finally влияет на возвращаемое значение: по умолчанию, если в finally ничего не предпринимать, результат, сформированный в try или catch через raise, сохраняется. Но если внутри finally будет вызван raise, он переопределит ранее подготовленный результат. То есть raise в finally имеет приоритет: он заставляет всё выражение вернуть то значение, которое указано в finally, даже если до этого в try или catch уже был выполнен raise с другим значением. Это поведение аналогично тому, как в сегодняшнем JavaScript блок finally может перезаписать результат функции, если внутри него сделать return или выбросить новое исключение (что, кстати, считается нежелательной практикой, но технически возможно).

Доступ к поднятому значению в finally:

Возникает вопрос – как finally может узнать, какое значение было поднято из try или catch? Предлагается расширение синтаксиса: блок finally может объявить псевдопараметр, например: finally (raised) { ... }. Переменная (идентификатор) в скобках после ключевого слова finally будет доступна внутри блока и содержать значение, переданное последним вызовом raise в try или catch. Если исключение произошло, а catch поднял значение, raised будет равно этому значению; если исключения не было и try поднял значение – соответственно тому. Если ни один из блоков не вызывал raise (то есть на самом деле выражение не вернуло ничего), raised может быть undefined. Это дает возможность в finally например логгировать или модифицировать результат перед окончательным возвратом. Блок finally при этом не обязан вызывать raise – он может просто выполнить побочные действия. Но если внутри finally все же вызвать raise, то новое значение замещает собой прежнее. Пример:

const data = try {
  raise fetchData();
} catch (e) {
  raise null;
} finally (result) {
  console.log("Результат перед выходом:", result);
  if (result === null) {
    // если данные не получены, переопределим результат на заглушку
    raise { status: "empty" };
  }
  // иначе не вызываем raise, и значение из try/catch пройдет далее
};

В этом коде мы всегда логируем полученный результат (успешный или null при ошибке). Затем, если результат равен null (т.е. произошла ошибка и catch вернул null), мы решаем переопределить итоговый результат на объект { status: "empty" } посредством raise в finally. Если же результат не null, finally ничего не возвращает, и значение из try (результат fetchData()) пройдет наружу. Таким образом, data окажется либо объектом с данными (при успешном запросе), либо объектом { status: "empty" } (если произошла ошибка).

Ключевое слово raise:

Предполагается ввести новое ключевое слово raise в язык. Оно выступает как управляющая конструкция, не как функция. С точки зрения грамматики, raise будет аналогично return или throw – за ним должно следовать выражение (или семантически допустимо опустить выражение, чтобы вернуть undefined, как return;). Использование raise вне контекста блока try/catch (который предназначен для возвращения значения) будет синтаксической ошибкой. Также нельзя использовать raise внутри вложенных функций, генераторов или arrow-функций вместо return – его действие ограничено именно текущим try-выражением. Если попытаться написать:

function f() {
  try {
    raise 42;
  } catch { raise -1; }
  // ...
  return 0;
}

то поведение будет следующим: когда f() вызвана, внутри нее выполнится try-выражение. Столкнувшись с raise 42, оно завершит это try-выражение и вернет 42 наружу – в нашем случае, внутрь из функции f(), но так как результат выражения не был присвоен и использован, выполнение дойдет до return 0 и f() вернет 0. Здесь важно разграничить: return выходит из функции, throw выходит из функции (если не пойман), а raise выходит только из блока try/catch-выражения. Это подобно тому, как оператор yield в Java вызывает выход лишь из switch-выражения, а не из окружающей функции stackoverflow.com.

Допустимые возвращаемые значения:

Вызов raise может "поднять" любое значение. Это может быть примитив (число, строка, и т.д.), объект, null или undefined. Нет ограничений или специальных требований, как к throw (который зачастую используют с объектами Error, но формально тоже может бросить что угодно). raise просто берет указанное выражение и делает его результатом. Даже raise undefined; считается корректным – это явный способ вернуть "ничего". Если raise написать без выражения (как отдельную инструкцию raise;), по аналогии с return; это будет означать поднять undefined.

Асинхронность и await:

Как показано ранее, raise полноценно работает в сочетании с await внутри async-функций. При использовании await результат промиса будет получен, а затем передан наружу. Это эквивалентно тому, как return await обрабатывает значение: сначала ждет, затем возвращает его. Важно подчеркнуть: если внутри try используется await, то и сам try-блок становится асинхронным, но поскольку весь код находится внутри async функции, это естественно. Нет дополнительных ограничений – можно писать raise await somePromise;. Если обещание (промис) разрешится нормально, его значение станет результатом, если отклонится (throw внутри промиса), то будет брошено исключение и, возможно, перехвачено блоком catch данного выражения.

Правила области видимости (scoping)

Ключевое слово raise не вводит новой области видимости – оно действует внутри уже существующей области того блока, где находится. Переменные, объявленные внутри try или catch (через let/const), видимы до конца этого блока (включая до его закрывающей фигурной скобки). Вызов raise может использовать любую переменную, доступную в данной области (как локальную, так и внешнюю). Однако после выполнения raise дальнейшие инструкции блока не выполняются, что важно учитывать: например, если ниже по коду определена еще какая-то переменная, она никогда не будет инициализирована, что эквивалентно ситуации с преждевременным return. Это не создает новых проблем, аналогичная ситуация бывает с return или throw. Просто разработчику и инструментам нужно будет следить за тем, чтобы код после raise не содержал критически важных операций (lint-правила могут подсвечивать "unreachable code after raise", подобно тому, как делают для return).

Отдельно стоит сказать про Temporal Dead Zone (TDZ). Появление try-выражений не меняет существующих правил TDZ для let/const. Но могут быть тонкости, если try-выражение используется в месте объявления переменной. Рассмотрим пример:

// Неправильно:
let value = try {
  raise someVar;
} catch (e) {
  console.log(e); // ReferenceError: Cannot access 'someVar' before initialization
  raise 1;
};
let someVar = 5;

Здесь мы пытаемся использовать someVar внутри инициализации value до того, как someVar объявлена. Разработчикам нужно по-прежнему следить за порядком объявлений. В остальном try-выражение не вводит новых ограничений: оно вычисляется последовательно, и любые переменные, объявленные внутри него, живут только в нем и не "просачиваются" наружу (наружу выходит лишь значение). Переменная, объявленная во внешней функции с тем же именем, что и идентификатор в finally (ident), не находится в конфликте – идентификатор в скобках finally работает как параметр, подобно параметру функции или названному exception-параметру в catch.

Потенциальные сложности и влияние на экосистему

Внедрение нового синтаксиса – особенно с введением нового ключевого слова – требует внимательного отношения к деталям реализации и обратной

Внедрение нового синтаксиса – особенно с введением нового ключевого слова – требует внимательного отношения к деталям реализации и обратной совместимости.

  • Обратная совместимость (ключевое слово raise): На сегодняшний день слово raise не является зарезервированным в JavaScript. Это означает, что существующий код мог использовать raise как имя переменной, функции, свойства объекта и т.д. Введение его в качестве ключевого слова теоретически может сломать такой код. Например, если где-то определено var raise = 5; или функция function raise(x) { ... }, то после появления ключевого слова это станет синтаксической ошибкой или будет восприниматься иначе. Чтобы минимизировать ущерб, можно рассмотреть контекстное ключевое слово: сделать так, что raise распознается как ключевое только внутри конструкций try { ... } catch { ... } finally { ... }, где по грамматике ожидается возврат значения. Подход с контекстными (или "ограниченными") ключевыми словами уже применялся: например, yield в ES6 стал ключевым словом только внутри генераторов, await – только внутри async функций или модулей. Вероятно, raise может быть сделан зарезервированным в строгом режиме или только в специальных контекстах, чтобы не сломать существующие скрипты. Но в любом случае это обсуждаемо: возможно, придется выбрать другое слово или подход (альтернативы рассмотрим ниже).
  • Поддержка инструментов (lint, форматтеры, IDE): Появление нового синтаксиса потребует обновления инструментов разработки. Правила линтеров (ESLint и др.) должны будут научиться корректно парсить try как выражение и не ругаться, например, на присваивание в const через try { ... } catch { ... }. Также могут появиться новые правила: например, предупреждение о коде после raise (как упоминалось), или требование, чтобы и в try, и в catch присутствовал raise (это, скорее, синтаксический уровень, но линтер может помогать ловить такие ошибки понятнее). Форматтеры кода (Prettier и др.) тоже должны знать про новый синтаксис, чтобы правильно расставлять отступы. В средах вроде VSCode появятся подсветка и автодополнение. Это все рабочие моменты, которые сопровождают любую новую возможность языка.
  • Изменения в парсерах и AST: JavaScript-движки (V8, SpiderMonkey, JavaScriptCore) должны будут адаптировать парсер под новый конструктив. Появится необходимость распознавать конструкцию try как часть выражения. По спецификации, скорее всего, будет введено что-то вроде TryExpression. AST (Abstract Syntax Tree) форматы, такие как ESTree, тоже расширятся новым типом узла, например TryExpression с узлами block, handler (catch) и finalizer. Внутри него новые узлы RaiseExpression для точек выхода. Инструменты, работающие с AST (бабели, трансформеры, minifiers) тоже потребуют обновления. Хотя изменение не тривиальное, оно локализовано: не затрагивает тонны существующей семантики, а лишь добавляет новый вид выражения и оператор. Близкий по масштабу пример – добавление опциональной цепочки (a?.b), которое тоже потребовало внести новый тип узла. Сообщество обычно справляется с такими изменениями достаточно быстро, особенно если фича популярна.
  • Влияние на читаемость и соглашения: Хотя цель предложения – улучшить читаемость, у него могут быть и критики. Некоторые разработчики могут найти непривычным видеть try/catch внутри выражения. Будут вопросы стиля: можно ли злоупотреблять и создавать слишком громоздкие выражения? Будет ли код сложнее отладки, если в одной строке много всего, включая обработку ошибок? Эти проблемы не технические, но влияют на принятие. Вероятно, появятся рекомендации (в документации или сообществе), как уместно использовать try-выражения, а когда лучше оставить явный try/catch блок. Например, правило: если логика обработки исключений сложная (больше нескольких строк), лучше не вкладывать ее в тернарный оператор, а писать отдельно. Или: избегать вложенных try-выражений ради читабельности. Эти аспекты находятся вне рамок спецификации, но важно иметь их в виду.
  • Производительность: В текущем виде try/catch в JavaScript несет незначительные издержки, особенно когда исключения не происходят. Введение try-выражений с raise скорее всего аналогично по затратам: пока не брошено исключение, выполнение идет линейно, с парой дополнительных проверок на raise. Если raise вызывается, это примерно то же, что return из функции – мгновенное завершение блока. Возможно, реализация затронет механизм completion records в спецификации (уже сейчас у каждого блока есть значение завершения). Тонкая оптимизация может потребоваться, но ничего не указывает на серьезное падение производительности. Тем не менее, движкам надо будет обработать новые пути выхода (например, в JIT-компиляции), чтобы эффективно предсказывать и инлайнить такие вещи. Это работа оптимизаторов, но особых препятствий нет.

Аналоги в других языках

Идея делать конструкции вроде try/catch вычисляемыми выражениями не нова. В разных языках реализовано разными путями:

Kotlin: В Kotlin блочный оператор try/catch является выражением и может возвращать значение. Конструкция очень похожа на предлагаемую, но без специального ключевого слова – результатом считается последнее выражение, выполненное в блоке try либо в блоке catch kotlinlang.org. Например, в Kotlin:

val num = try {
    riskyOperation()
} catch (e: Exception) {
    -1
}

Здесь num получит либо результат riskyOperation(), либо -1 в случае исключения. Блок finally в Kotlin, хотя и выполняется всегда, не влияет на результат выражения (его используют только для побочных эффектов) kotlinlang.org. Наше предложение для JavaScript, по сути, стремится к аналогичному удобству, но более явным синтаксисом. Мы вводим raise чтобы явно обозначать возвращаемое значение, вместо неявного "последнего выражения". Это решение принято из соображений ясности: явный ключевой слово снижает вероятность ошибки, когда, например, кто-то забыл, что надо вернуть значение, или случайно разместил ненужный код после нужного выражения.

Scala: В языке Scala try-catch-finally также является выражением. Поскольку Scala – язык выражений, там практически все конструкции могут возвращать значение. Семантика как у Kotlin: результат – это либо значение из try, либо из catch. Отличие Scala в том, что catch там основан на сопоставлении с образцом (pattern matching), но суть та же. Кроме того, в Scala имеется класс Try (в библиотеке), представляющий результат выполнения, который может быть успешным (Success) или содержать исключение (Failure). Он позволяет писать код в функциональном стиле: вместо использования ключевых слов, оборачивает результат в объект. Например:

import scala.util.Try
val resultTry = Try(riskyOperation())  // вернет Success(значение) или Failure(ошибка)
val finalVal = resultTry.getOrElse(defaultValue)

Такой подход похож по цели – избавиться от явного try/catch при присваивании – но реализован через библиотечный класс. Наш путь – интегрировать удобство на уровне языка, без дополнительных объектов-оберток.

Rust: В Rust отсутствуют исключения, поэтому напрямую аналога try-catch нет, но есть результатный тип ResultOption) и оператор ? для распространения ошибок. Интересно, что в версии Rust 2018+ появилось понятие try-блока (пока экспериментального, на момент появления) doc.rust-lang.org. Try-блок в Rust выглядит как:

let result: Result<T, E> = try {
    let x = doSomething()?;   // если doSomething() вернет Err, то выходим из try-блока
    let y = doOther()?;       // аналогично, ? оператор возвращает из блока при ошибке
    compute(x, y)
};

Здесь, если внутри блока встречается ?, он немедленно прерывает блок, возвращая Err(...) как значение всего блока. Если же ни один ? не сработал, результатом будет Ok(...) с последним выражением (в примере compute(x,y)). Таким образом, Rust решает сходную задачу – получить из блока либо успех, либо ошибку – но делая это через типизацию (Result) вместо исключений, а try {} + ? служат синтаксическим сахаром. Для нас Rust интересен тем, что демонстрирует: блок-выражение с ранним выходом по специальному оператору – концепция не чуждая современным языкам. По аналогии, raise в JavaScript – это "ранний выход" с готовым значением, не приводящий к выбросу исключения.

Python (PEP 463): Как упоминалось, в Python был предложен синтаксис для "exception-catching expressions". Идея заключалась в том, чтобы позволить конструкцию вида:

result = expr1 except SomeException: expr2

Это бы означало: вычислить expr1, и если возникает SomeException, вместо него взять значение expr2. Предложение (PEP 463) обосновывалось тем, что в Python не хватает выражения для EAFP-стиля (Easier to Ask Forgiveness than Permission) прямо внутри других выражений peps.python.org. Например:

process(value if key in dict else default)      # LBYL стиль (есть тернарный оператор)
process(dict[key] except KeyError: default)     # Предлагаемый EAFP стиль

Хотя PEP 463 был вежливо отклонен Guido ван Россумом (отчасти из опасений усложнения языка), он является показателем потребности. Однако предлагаемое решение в JS несколько отличается синтаксисом. Python-предложение реализовывало это более лаконично (буквально как инфиксный оператор except внутри выражения). Мы же в JavaScript стараемся сохранить привычную форму try { } catch { } даже в выражении, и вводим raise вместо перегрузки существующего слова. Это выглядит более громоздко чем PEP 463, но лучше вписывается в синтаксис JS. Стоит отметить, что Python до сих пор не имеет никаких try-выражений — то есть разработчики по-прежнему используют обычные try/except блоки.

Другие языки: В ряде функциональных и скриптовых языков есть схожие механизмы. Например, OCaml/F# (ML-семейство) – там try...with (эквивалент catch) является выражением, возвращающим значение, поскольку в этих языках вообще все конструкции возвращают значение. Haskell не имеет исключений как основного механизма, но есть монада Either/IO для обработки ошибок без выброса. Go пошел по пути вообще отказаться от исключений, возвращая ошибку как второй результат функции. В контексте JavaScript, где исключения есть и активно используются, наш подход ближе к Kotlin/Scala – сделать их использование более гибким и менее шаблонным.

Заключение

В этом документе был представлен набросок предложения для TC39, позволяющего использовать try/catch/finally как выражение с возвращаемым значением, благодаря введению нового ключевого слова raise. Мы рассмотрели мотивацию (упрощение шаблонного кода, улучшение декларативности), детально описали предполагаемый синтаксис и поведение raise, включая правила его использования и взаимодействия с finally. Также были обсуждены потенциальные подводные камни (ключевое слово, AST, линтеры, совместимость) и показано, что подобные возможности существуют или обсуждались в других языках (Kotlin, Scala, Rust, Python и др.), что подтверждает ценность идеи. Наконец, проанализированы альтернативные подходы и обосновано, почему выбран именно данный путь.

Призыв к обсуждению: На стадии 0 важно собрать мнения и выявить возможные проблемы, которые не покрыты черновиком. Открыты вопросы могут включать:

  • Выбор ключевого слова: подходит ли raise по смыслу и нет ли риска путаницы с "raise exception" из других языков?
  • Нужен ли параметр в finally или можно обойтись без нового синтаксиса вроде finally (raised)?
  • Все ли случаи учтены (например, поведение при вложенных try-выражениях, при использовании break/return внутри блоков и т.п.)?
  • Насколько часто такая конструкция пригодится на практике, есть ли реальные примеры из кода, где это существенно улучшит качество?

Обсуждение этих вопросов, а также эксперименты с прототипированием (в Babel либо в отдельных ветках V8) помогут продвинуть предложение к Stage 1 и дальше, если сообщество посчитает идею полезной. Мы надеемся, что предоставленный explainer ясно передает суть и преимущества try-выражений с raise, и с нетерпением ждем обратной связи от TC39 и сообщества JavaScript разработчиков.

Ссылка на начальный драфт пропозала:

https://github.com/dmitrytarassov/tc39-raise-proposal-draft?tab=readme-ov-file