Паттерны ошибок
Ошибка как enum#
Задача. Описать несколько причин отказа одним типом и разобрать их в одном match.
@error enum PayError {
Declined(reason: str)
Fraud
LimitExceeded(max: i64)
}
fn charge(amount: i64) -> i64 | PayError {
if amount > 1000 { fail PayError.LimitExceeded(1000) }
if amount == 13 { fail PayError.Fraud }
if amount < 0 { fail PayError.Declined("negative amount") }
amount
}
fn describe(r: i64 | PayError) -> str =>
match r {
i64 n => "charged {n}"
PayError.Declined(reason) => "declined: {reason}" // варианты enum-ошибки разбираются как обычные
PayError.Fraud => "fraud suspected"
PayError.LimitExceeded(max) => "over the limit of {max}"
}
fn main() io {
for a in [50, 13, -5, 5000] { print(describe(charge(a))) }
}Вывод
charged 50 fraud suspected declined: negative amount over the limit of 1000
@error enum PayError { Declined(reason: str); Fraud; LimitExceeded(max: i64) }— одна «семья» ошибок; варианты несут данные.- Функция возвращает
i64 | PayError, аmatchразбирает и успех, и каждый вариант. Забытый вариант —non-exhaustive. - Когда использовать
enum, а когда несколько@error data:enum— если вызывающий всегда обрабатывает все причины вместе; отдельные типы — если разные вызывающие интересуются разными ошибками и объединение растёт по цепочке вызовов.
Повтор с задержкой#
Задача. Повторить нестабильную операцию несколько раз, не повторяя бесконечно.
@error data Flaky { attempt: i32 }
fn unreliable(attempt: i32) -> str | Flaky {
if attempt < 3 { fail Flaky{ attempt } }
"ok on attempt {attempt}"
}
fn withRetry(max: i32) -> str | Flaky io, time {
var attempt = 1
loop {
match unreliable(attempt) {
str s => break s
Flaky e => {
if attempt >= max { fail e } // попытки кончились — отдаём последнюю ошибку
print("attempt {attempt} failed, retrying")
sleep(Duration.ofMillis(10 * attempt as i64)) // линейная задержка между попытками
attempt += 1
}
}
}
}
fn main() io, time {
match withRetry(5) {
str s => print(s)
Flaky e => print("gave up after {e.attempt}")
}
match withRetry(2) {
str s => print(s)
Flaky e => print("gave up after {e.attempt}")
}
}Вывод
attempt 1 failed, retrying attempt 2 failed, retrying ok on attempt 3 attempt 1 failed, retrying gave up after 2
loop { … break value }— цикл-выражение:break sотдаёт успешное значение,fail e— последнюю ошибку, когда попытки кончились.- Задержка растёт с номером попытки (
10 * attemptмс); для сетевых rpc то же делают декларативно атрибутами@retry(n, backoff: …)— но только для@idempotentи@readonlyметодов, чтобы повтор не выполнил действие дважды. - Подводный камень: повторяйте только временные ошибки (
Timeout,Unavailable). Ошибку «неверный ввод» повтор не исправит.
Запасной путь#
Задача. Если значения нет в кэше — взять из базы; если база недоступна — вернуть ошибку.
@error data CacheMiss { key: str }
@error data DbDown { why: str }
fn fromCache(key: str) -> str | CacheMiss {
if key == "a" { return "cached-a" }
fail CacheMiss{ key }
}
fn fromDb(key: str) -> str | DbDown {
if key == "c" { fail DbDown{ why: "connection refused" } }
"db-{key}"
}
fn load(key: str) -> str | DbDown {
await fromCache(key)
!> CacheMiss => fromDb(key)? // !> заменяет ошибку запасным путём
}
fn main() io {
for k in ["a", "b", "c"] {
match load(k) {
str s => print("{k}: {s}")
DbDown e => print("{k}: db down ({e.why})")
}
}
}Вывод
a: cached-a b: db-b c: db down (connection refused)
await fromCache(key) !> CacheMiss => fromDb(key)?— обработчик!>срабатывает только наCacheMiss; другие ошибки (здесьDbDown) проходят дальше по цепочке.- Тип результата
load—str | DbDown:CacheMissиз него исчез, потому что обработан. Компилятор это проверяет. - Несколько обработчиков можно ставить подряд:
!> A => … !> B => ….
Освобождение ресурсов: defer#
Задача. Гарантированно закрыть ресурс, как бы ни завершилась функция.
fn process(name: str) -> str | Failed io {
print("open {name}")
defer print("close {name}") // defer выполняется при любом выходе из функции
if name == "bad" { fail Failed{} }
print("work {name}")
"done {name}"
}
@error data Failed {}
fn main() io {
for n in ["good", "bad"] {
match process(n) {
str s => print(s)
Failed => print("failed {n}")
}
}
}Вывод
open good work good close good done good open bad close bad failed bad
defer выражениевыполняется при любом выходе из функции: нормальном, черезfail, через?и даже при панике.- Несколько
deferвыполняются в обратном порядке (последний — первым), как стек вызовов. - Типы с
@must_close(Server,WebSocket, …) требуют закрытия: компилятор сообщаетmust-close, если вы забылиdefer x.close().
Вернуться: кулинарная книга.