Skip to content

Обработка ошибок

This content is not available in your language yet.

В Python исключение может прилететь из любой строки: вы никогда не знаете заранее, бросит функция ошибку или нет, пока не прочитаете её код целиком вместе со всем, что она вызывает.

В Mojo способность бросать исключение — часть сигнатуры. Компилятор следит за этим так же строго, как за типами.

Функция, которая может бросить исключение, обязана это объявить:

def parse(text: String) raises -> Int:
return Int(text)

Слово raises ставится после круглых скобок и перед стрелкой. Забудете — компилятор не пропустит:

error: 'raise' requires a surrounding 'try' block or the enclosing function to declare 'raises'
Что это значит

Внутри функции есть raise, но в сигнатуре нет raises. Обещание и поведение разошлись.

Как исправить

Либо допишите raises в сигнатуру и переложите обработку на вызывающий код, либо обработайте ошибку прямо здесь, в try / except.

Обещание работает в обе стороны. Вызвать бросающую функцию из обычной нельзя:

error: cannot call function that may raise in a context that cannot raise
Что это значит

Вы вызвали функцию с raises из функции без raises и без try. Ошибке некуда деваться.

Как исправить

Компилятор сам подсказывает два выхода — они же и есть единственные:

note: try surrounding the call in a 'try' block
note: or mark surrounding function as 'raises'

Первое — обработать здесь. Второе — передать ответственность выше.

Именно поэтому в примерах курса нередко встречается def main() raises: — это верхний этаж, дальше передавать некому.

🐍 Python
def load(path): # бросает или нет?
return open(path).read()
🔥 Mojo
def load(path: String) raises -> String:
... # видно из сигнатуры

В Python, чтобы узнать это, нужно прочитать тело функции и всё, что она вызывает. В Mojo достаточно одной строки сигнатуры — и компилятор не даст ей соврать.

raise принимает значение типа Error — или просто строку, которая в него превратится:

def check(age: Int) raises:
if age < 0:
raise Error("возраст отрицательный")
if age > 150:
raise "возраст слишком большой"
def main():
try:
check(-5)
except e:
print(e)
Результат

возраст отрицательный

Своих классов исключений в Mojo 1.0 нет:

error: cannot implicitly convert 'ParseError' value to 'Error'
Что это значит

Вы попытались бросить собственную структуру. Иерархии исключений, как в Python, в языке пока нет — тип ошибки единственный.

Как исправить

Различайте ошибки по тексту сообщения либо, если различие важно для логики, возвращайте результат через Optional или собственный тип-результат вместо исключения.

Практический приём — префикс в сообщении:

def check(age: Int) raises:
if age < 0:
raise Error("ввод: возраст отрицательный")
def main():
try:
check(-5)
except e:
if String(e).startswith("ввод:"):
print("это ошибка пользователя, попросим ввести заново")
else:
print("это сбой программы:", e)
Результат

это ошибка пользователя, попросим ввести заново

Все четыре блока работают как в Python, но синтаксис except короче:

def risky(fail: Bool) raises:
if fail:
raise Error("плановый сбой")
def main() raises:
try:
print("пробуем")
risky(True)
except e:
print("except:", e)
else:
print("else: ошибок не было")
finally:
print("finally: выполняется всегда")
Результат

пробуем except: плановый сбой finally: выполняется всегда

  • except e: — ошибка попадает в переменную e;
  • except: — без переменной, когда текст не нужен;
  • else: — выполняется, только если исключения не было;
  • finally: — выполняется в любом случае.
error: expected ':' after 'except'
Что это значит

Вы написали питоновское except Error as e:. В Mojo такой формы нет — ловить по типу нечего, тип всегда Error.

Как исправить

Пишите просто except e: — переменная указывается сразу, без as.

Приятный побочный эффект строгости: try вокруг кода, который не бросает, не остаётся незамеченным.

warning: 'except' logic is unreachable, try doesn't raise an exception

Это предупреждение стоит воспринимать как подсказку: либо вы обернули не ту строку, либо try здесь лишний. В Python такую ошибку не находит никто, и мёртвый except живёт в коде годами.

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

var xs: List[Int] = [1, 2, 3]
print(xs[10])
Результат

At: main.mojo:3:13: Assert Error: index 10 is out of bounds, valid range is 0 to 2 … mojo: error: execution crashed

Программа падает сразу, и try / except вокруг такой строки не поможет — более того, компилятор выдаст на него то самое предупреждение «try doesn’t raise an exception».

Свои проверки такого рода пишутся через debug_assert. Но есть тонкость, которую стоит узнать сразу: по умолчанию они не выполняются.

def main():
var x = -5
debug_assert(x > 0, "x должен быть положительным")
print("после проверки")
Результат

после проверки

Условие ложно, а программа спокойно идёт дальше. Это не ошибка, а настройка: уровень проверок задаётся флагом -D ASSERT.

ЗначениеЧто включеноКогда использовать
safeпроверки стандартной библиотеки: границы списков и подобноепо умолчанию, обычная работа
allто же плюс ваши debug_assertотладка и тесты
noneничегозамер производительности отлаженного кода
Окно терминала
uv run mojo -D ASSERT=all main.mojo
Результат

At: main.mojo:3:17: Assert Error: x должен быть положительным … mojo: error: execution crashed

Часто «значения нет» — это не ошибка, а обычный исход. Для таких случаев есть Optional, доступный без импорта:

def main():
var found = Optional[Int](42)
var missing = Optional[Int]()
if found:
print("есть значение:", found.value())
if not missing:
print("значения нет")
print(missing.or_else(0))
Результат

есть значение: 42 значения нет 0

Стандартная библиотека и сама этим пользуется — например, Dict.get() не бросает исключение, а возвращает Optional:

def main():
var codes = Dict[String, Int]()
codes["Москва"] = 495
print(Bool(codes.get("Казань")))
print(codes.get("Москва", 0))
Результат

False 495

Сравните с обращением по ключу codes["Казань"] — оно бросает исключение и требует raises. Оба варианта в языке есть, и выбирать вам.

СитуацияИнструмент
Значения может не быть — это нормальноOptional
Что-то пошло не так, и вызывающий код должен узнать причинуraise
Ошибка в самом коде, программа дальше бессмысленнаdebug_assert

Правило простое: если по итогам нужно объяснение, бросайте исключение. Если достаточно ответа «нет» — берите Optional, он дешевле и не заражает raises всю цепочку вызовов.

with работает как в Python: нужен тип с методами __enter__ и __exit__.

@fieldwise_init
struct Section(ImplicitlyCopyable):
var title: String
def __enter__(self) -> Self:
print("--", self.title)
return self
def __exit__(self):
print("-- конец:", self.title)
def main():
try:
with Section("замер"):
print("работаем")
raise Error("сбой внутри with")
except e:
print("поймали:", e)
Результат

— замер работаем — конец: замер поймали: сбой внутри with

Обратите внимание на порядок: __exit__ вызвался до того, как ошибка дошла до except. Ради этого with и существует — что бы ни случилось внутри блока, файл закроется, замок освободится, замер завершится.

Программа завершается с ненулевым кодом и печатает две строки — сначала подсказку, потом само сообщение:

stack trace was not collected. Enable stack trace collection with environment variable `MODULAR_DEBUG=stack-trace-on-error`
Unhandled exception caught during execution: String is not convertible to integer with base 10: 'не число'

Подсказка честная, но с оговорками. Стек действительно собирается, если задать переменную:

Окно терминала
MODULAR_DEBUG=stack-trace-on-error uv run mojo main.mojo

Только в таком виде это будут голые адреса без имён функций. Читаемый стек с именами и номерами строк даёт лишь отдельно собранный бинарник с отладочной информацией:

Окно терминала
uv run mojo build -debug-level=full main.mojo
MODULAR_DEBUG=stack-trace-on-error ./main
safe_parse.mojo
# Разбор пользовательского ввода: исключения, Optional и with.
@fieldwise_init
struct Section(ImplicitlyCopyable):
"""Печатает заголовок и подводит черту, что бы ни случилось внутри."""
var title: String
def __enter__(self) -> Self:
print("--", self.title)
return self
def __exit__(self):
print("-- конец:", self.title)
def parse_age(text: String) raises -> Int:
"""Превращает строку в возраст или объясняет, почему это невозможно."""
var value = Int(text)
if value < 0:
raise Error("возраст отрицательный")
if value > 150:
raise Error("возраст слишком большой")
return value
def parse_or_default(text: String, fallback: Int) -> Int:
"""Возвращает разобранное значение или запасное, не роняя программу."""
try:
return parse_age(text)
except:
return fallback
def try_parse(text: String) -> Optional[Int]:
"""Возвращает значение, если разбор удался, и пустой Optional иначе."""
try:
return Optional[Int](parse_age(text))
except:
return Optional[Int]()
def main():
var inputs: List[String] = ["30", "abc", "-5", "200"]
with Section("подробный разбор"):
for text in inputs:
try:
print(text, "->", parse_age(text))
except e:
print(text, "-> отклонено:", e)
with Section("со значением по умолчанию"):
for text in inputs:
print(text, "->", parse_or_default(text, 18))
with Section("через Optional"):
for text in inputs:
var result = try_parse(text)
if result:
print(text, "-> значение", result.value())
else:
print(text, "-> значения нет")
with Section("порядок выполнения"):
for text in ["7", "нет"]:
try:
print("пробуем:", text)
print("разобрали:", parse_age(text))
except e:
print("except:", e)
else:
print("else: ошибок не было")
finally:
print("finally: выполняется всегда")
Результат

— подробный разбор 30 -> 30 abc -> отклонено: String is not convertible to integer with base 10: ‘abc’ -5 -> отклонено: возраст отрицательный 200 -> отклонено: возраст слишком большой — конец: подробный разбор — со значением по умолчанию 30 -> 30 abc -> 18 -5 -> 18 200 -> 18 — конец: со значением по умолчанию — через Optional 30 -> значение 30 abc -> значения нет -5 -> значения нет 200 -> значения нет — конец: через Optional — порядок выполнения пробуем: 7 разобрали: 7 else: ошибок не было finally: выполняется всегда пробуем: нет except: String is not convertible to integer with base 10: ‘нет’ finally: выполняется всегда — конец: порядок выполнения

🎯 Проверь себя

Чем `raises` в Mojo отличается от исключений в Python?

В Python бросить исключение может любая функция, и узнать об этом можно только прочитав её код. В Mojo способность бросать исключение — часть сигнатуры: функция объявляет raises, а вызывающий код обязан либо обернуть вызов в try, либо тоже объявить raises.

Можно ли поймать выход за границы списка через try/except?

Нет. xs[10] для списка из трёх элементов — это Assert Error, программа падает сразу. Исключения предназначены для ожидаемых ситуаций, а выход за границу — ошибка в коде. Компилятор даже предупредит, что except вокруг такой строки недостижим.

Когда лучше вернуть `Optional`, а когда бросить исключение?

Если «значения нет» — нормальный исход и объяснять нечего, берите Optional: он не заставляет всю цепочку вызовов объявлять raises. Если нужно передать причину сбоя — бросайте исключение.

Как объявить собственный тип исключения?

Никак: в Mojo 1.0 тип ошибки единственный — Error. Различают ошибки по тексту сообщения (например, по префиксу) либо возвращают результат другим способом — через Optional или свой тип-результат.

Структуры: собственные типы, поля, методы и то, чем struct принципиально отличается от питоновского class.

Примеры проверены на Mojo 1.0.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0