Перейти к содержанию

Result и ошибки на границе JavaScript

Чистая функция Rust уже считает статистику текста. Теперь добавим два условия: файл не превышает восемь MiB и его содержимое является корректным UTF-8. Нарушение договора должно давать управляемую ошибку, а не случайное прерывание модуля или набор чисел для незаметно исправленного текста.

Работа распределяется между сторонами. JavaScript читает выбранный файл и декодирует байты. Rust повторно проверяет объём полученного текста и выполняет вычисление. Обе проверки полезны: предварительная экономит ресурсы, внутренняя защищает сам экспорт от вызова через другое место приложения.

Ошибка как часть результата

В Rust тип Result<T, E> означает либо успешное значение T, либо ошибку E. Это позволяет явно обработать отказ. Для нашей внутренней функции ошибкой будет короткая статическая строка; она не содержит текст файла и другие пользовательские данные.

К прежнему src/lib.rs добавим полный новый блок. Тип Stats и функция analyze остаются из предыдущего урока:

pub const MAX_BYTES: usize = 8 * 1024 * 1024;

pub fn analyze_checked(text: &str) -> Result<Stats, &'static str> {
    if text.len() > MAX_BYTES {
        return Err("Text exceeds the 8 MiB limit");
    }
    Ok(analyze(text))
}

Вызов возвращает Ok только после проверки. Код не использует panic! для обычного неподходящего входа. Паника и ожидаемая предметная ошибка имеют разный смысл: слишком большой файл пользователь может заменить, а внутренний сбой требует диагностики самого приложения.

Лимит относится к байтам, а не к длине JavaScript-строки. Русские буквы и emoji имеют другое кодирование, поэтому проверка text.length < 8 * 1024 * 1024 не выражает тот же договор. На стороне Rust text.len() уже измеряет UTF-8-байты. На стороне файла исходный размер известен до декодирования.

Экспорт с понятным внешним форматом

Через wasm-bindgen экспортируем проверенную функцию и вернём пять чисел в согласованном порядке. Используем JsError, чтобы отказ стал объектом JavaScript Error. Этот тип предусмотрен API wasm-bindgen.

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsError;

#[wasm_bindgen]
pub fn analyze_text(text: &str) -> Result<Vec<u32>, JsError> {
    let stats = analyze_checked(text).map_err(JsError::new)?;
    Ok(vec![
        stats.bytes as u32,
        stats.scalars as u32,
        stats.lines as u32,
        stats.non_empty_lines as u32,
        stats.max_line_scalars as u32,
    ])
}

Знак ? останавливает функцию при ошибке и передаёт её вызывающему коду. После него stats обозначает успешное значение. Приведение к u32 допустимо в нашем ограниченном примере: все счётчики не превосходят размерного порядка восьми MiB с небольшим числом разделителей. Если лимит увеличится до многогигабайтных данных, этот аргумент нужно будет пересмотреть.

Порядок массива является частью версии API. JavaScript-адаптер преобразует его в объект с именованными полями и проверит длину. Переставлять поля ради внутреннего рефакторинга нельзя без согласованного изменения адаптера. Позднее можно выбрать более выразительный способ обмена, но небольшой массив позволяет ясно увидеть типизированную границу.

Строгое декодирование файла

Создадим web/input.mjs. Функция принимает объект File, выбранный пользователем. Она не открывает произвольный путь на диске и не отправляет содержимое по сети:

export const MAX_BYTES = 8 * 1024 * 1024;

export async function readTextFile(file) {
  if (!file) throw new Error("Choose a file");
  if (file.size > MAX_BYTES) throw new Error("File exceeds the 8 MiB limit");
  const bytes = new Uint8Array(await file.arrayBuffer());
  const decoder = new TextDecoder("utf-8", {
    fatal: true,
    ignoreBOM: true
  });
  return decoder.decode(bytes);
}

fatal: true запрещает незаметную замену неправильной последовательности байт. Если файл повреждён или записан в другой кодировке, декодирование завершится ошибкой. Приложение предложит корректный UTF-8, а не покажет статистику для изменённого текста. Автоматическое определение любой кодировки не входит в договор «Меры».

Параметр ignoreBOM: true здесь означает, что декодер не использует BOM как специальный удаляемый маркер: символ U+FEFF сохраняется в результате. Поэтому общие байты и скалярные значения учитывают его. Название параметра легко прочитать неверно; в документации и интерфейсе нужно явно описать принятое поведение. Если захотим удалять BOM, это будет новое правило нормализации и изменит измеряемые данные.

Сообщение пользователю

Обработчик ошибки отделяется от вывода чисел. Не выводите NaN, нули или старую статистику под названием нового файла после отказа. Это создаёт правдоподобный, но неверный результат. При начале операции очищаем предыдущий вывод, а при отказе записываем короткое объяснение в область статуса.

try {
  const text = await readTextFile(file);
  const result = await engine.analyze(text);
  showResult(result);
} catch (error) {
  status.textContent = error instanceof Error ? error.message : String(error);
}

Это фрагмент будущего обработчика интерфейса. engine, showResult и status будут определены в уроке о загрузчике; здесь не выдаём его за самостоятельную полную программу. Текст ошибки передаётся через textContent, поэтому название файла или сообщение не превращаются в HTML.

Проверка расширения .txt не подтверждает кодировку и структуру содержимого. Пользователь может переименовать любой файл. accept в HTML облегчает выбор, но вычислительный договор проверяется по фактическим данным. По той же причине размер известного выбранного файла проверяем на стороне чтения и в модуле, а не только по внешней подписи.

Программный код ошибки и видимое сообщение

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

При развитии «Меры» можно описать собственный объект ошибки в JavaScript-адаптере. Он получает известный тип отказа из модуля, добавляет код и показывает локализованный текст. Не нужно сериализовать весь стек, источник файла и внутренние поля Rust в публичный статус. Для исправления неподходящего входа достаточно понятной причины и доступного действия.

Отдельно рассматривайте неожиданную неисправность. Если пакет не соответствует договору или Worker остановился, сообщение не должно обвинять пользователя в неверном UTF-8. Разделение чтения, подготовки и вычисления уже позволяет сохранить эту информацию. В рабочем интерфейсе полезны два уровня: краткое объяснение для читателя и техническая запись, которая не раскрывает содержимое выбранного документа.

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

Теперь у отказов есть точное место: чтение, декодирование, лимит или вычислительная функция. Мы не маскируем предметную ошибку успешной статистикой. В следующем уроке закрепим окружение и покажем, какие файлы создают Cargo и wasm-bindgen для браузера. Ни чтение файла, ни Rust-модуль при подготовке текста не запускались.

Оглавление серии