Обработка ошибок
This content is not available in your language yet.
В Python исключение может прилететь из любой строки: вы никогда не знаете заранее, бросит функция ошибку или нет, пока не прочитаете её код целиком вместе со всем, что она вызывает.
В Mojo способность бросать исключение — часть сигнатуры. Компилятор следит за этим так же строго, как за типами.
raises — обещание в сигнатуре
Заголовок раздела «raises — обещание в сигнатуре»Функция, которая может бросить исключение, обязана это объявить:
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' blocknote: or mark surrounding function as 'raises'Первое — обработать здесь. Второе — передать ответственность выше.
Именно поэтому в примерах курса нередко встречается def main() raises: —
это верхний этаж, дальше передавать некому.
def load(path): # бросает или нет? return open(path).read()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)это ошибка пользователя, попросим ввести заново
try / except / else / finally
Заголовок раздела «try / except / else / finally»Все четыре блока работают как в 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.mojoAt: main.mojo:3:17: Assert Error: x должен быть положительным … mojo: error: execution crashed
Optional — когда ошибка не нужна
Заголовок раздела «Optional — когда ошибка не нужна»Часто «значения нет» — это не ошибка, а обычный исход. Для таких случаев
есть 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_initstruct 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.mojoMODULAR_DEBUG=stack-trace-on-error ./mainВсё вместе
Заголовок раздела «Всё вместе»# Разбор пользовательского ввода: исключения, Optional и with.
@fieldwise_initstruct 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.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0