Отладка и надёжность
Как увидеть, что происходит внутри скрипта, узнать о ночном сбое до звонка бухгалтера и вернуться на прошлую версию за минуту.
Журнал выполнений (Executions) — список всех запусков скрипта за последние дни: кто запустил, сколько длился, чем кончился. Это единственное место, где видно ошибки триггеров и веб-приложения: вам о них никто не сообщит.
Почему обычная отладка тут не работает
Пока вы жмёте «Выполнить» в редакторе, всё просто: ошибка вылезает красной строкой. Но боевой скрипт запускается ночью по триггеру или из чужого браузера через веб-приложение. Никто на него не смотрит. Он падает — и молчит. Через неделю выясняется, что отчёт не обновлялся с прошлого вторника.
Поэтому «отладка» в Apps Script — это две разные задачи. Первая: разобраться, что творится в коде здесь и сейчас (логи, точки останова). Вторая: сделать так, чтобы о падении в 3 часа ночи вы узнали в 3:01, а не через неделю (журнал, try/catch, письмо себе).
Logger.log и console.log
Оба пишут в журнал выполнения, который открывается снизу редактора после запуска. Разница в том, что console.log (а также console.warn и console.error) отправляет запись ещё и в Google Cloud Logging — она сохраняется дольше и доступна из журнала выполнений для любых запусков, включая триггерные и веб-запросы. Правило простое: быстрая проверка руками — что угодно; в коде, который живёт на проде, — console.
function syncOrders() {
const orders = fetchOrders_();
console.log('Получено заказов: ' + orders.length);
if (!orders.length) {
console.warn('Пустой ответ API — проверьте фильтр по дате');
return;
}
// Объекты печатайте через JSON.stringify: иначе в журнале будет [object Object]
console.log('Первый заказ: ' + JSON.stringify(orders[0]));
}
Логи не бесплатны: каждая запись — это время. Не логируйте внутри цикла на 5000 итераций, иначе сами приблизите таймаут. Логируйте границы: сколько получили, сколько записали, сколько пропустили и почему.
Точки останова
В редакторе слева от номера строки есть узкая полоска — щёлкните по ней, появится синий кружок. Выберите функцию в списке сверху и нажмите «Отладка» вместо «Выполнить». Скрипт остановится на этой строке, и внизу откроется панель со всеми переменными: можно раскрыть объект, посмотреть массив, шагнуть на следующую строку (Step over) или зайти внутрь функции (Step into).
Это лучший способ понять, что реально приехало из UrlFetchApp или что лежит в e у триггера. Но у отладчика есть жёсткое ограничение: он работает только для запусков из редактора. Ни триггер, ни веб-запрос, ни формулу в ячейке пошагово не пройти — там остаются только логи. Отсюда практический приём: вынесите логику в обычную функцию с аргументами, а doPost(e) или обработчик триггера сделайте тонкой обёрткой. Тогда логику можно отладить, подсунув ей поддельный объект события.
function doPost(e) {
return handleRequest_(e.parameter); // тонкая обёртка
}
function handleRequest_(params) {
// вся логика здесь — её можно отлаживать
return ContentService.createTextOutput('ok: ' + params.name);
}
function debugHandleRequest() {
// запускаем в отладчике с поддельными данными
const res = handleRequest_({ name: 'Иванов', date: '2026-05-01' });
console.log(res.getContent());
}
Журнал выполнений
Слева в редакторе есть иконка «Выполнения». Там таблица всех запусков за последние дни.
| Колонка | Что смотреть |
| Тип запуска | Редактор, Триггер, Веб-приложение, Надстройка — сразу видно, кто дёрнул код. |
| Статус | Completed — отработал; Failed — исключение; Timed out — упёрся в лимит времени; Cancelled — остановлен вручную. |
| Длительность | Растёт от запуска к запуску? Данных стало больше — скоро таймаут. |
| Раскрытая строка | Все console.log этого запуска и стек ошибки. |
Google присылает владельцу сводку об упавших триггерах, но это письмо приходит с задержкой, легко теряется в почте и ничего не говорит о том, какие данные не доехали. Полагаться на него нельзя.
try/catch и письмо себе
Надёжный фоновый скрипт устроен так: обёртка ловит любое исключение, пишет его в журнал и шлёт вам письмо с текстом ошибки и стеком. Одна тонкость — не превратить это в самообстрел: если триггер падает каждые пять минут, за сутки вы получите под три сотни писем и выберете почтовую квоту. Поэтому уведомление отправляется не чаще раза в час, а сторожем работает CacheService.
function safeRun_(name, fn) {
try {
return fn();
} catch (err) {
console.error(name + ' упал: ' + err.stack);
notifyOnce_(name, err);
throw err; // пробрасываем: пусть запуск числится Failed в журнале
}
}
function notifyOnce_(name, err) {
const cache = CacheService.getScriptCache();
const key = 'err:' + name;
if (cache.get(key)) return; // за последний час уже писали
cache.put(key, '1', 3600); // молчим 3600 секунд
MailApp.sendEmail({
to: Session.getEffectiveUser().getEmail(),
subject: '[GAS] Сбой: ' + name,
body: [
'Таблица: ' + SpreadsheetApp.getActive().getName(),
'Функция: ' + name,
'Ошибка: ' + err.message,
'',
'Стек:',
err.stack
].join('\n')
});
}
// А вот так это выглядит в обработчике триггера
function dailySync() {
safeRun_('dailySync', () => {
const orders = fetchWithRetry_('https://api.example.com/orders');
writeOrders_(JSON.parse(orders.getContentText()));
});
}
err.stack ценнее err.message: он показывает файл и номер строки. Session.getEffectiveUser().getEmail() возвращает адрес того, чьими правами выполняется код, — то есть ваш, а не случайного посетителя веб-приложения. И обратите внимание на throw err в конце: без него запуск попадёт в журнал как успешный, и вы потеряете статистику падений.
Повторные попытки
Значительная часть «ошибок» — это не ошибки, а короткие сбои сети или ответ 429 «слишком часто». Такие вещи лечатся повтором с нарастающей паузой, и до письма дело просто не дойдёт.
function fetchWithRetry_(url, params) {
let wait = 1000;
for (let attempt = 1; attempt <= 5; attempt++) {
const options = Object.assign({ muteHttpExceptions: true }, params || {});
const res = UrlFetchApp.fetch(url, options);
const code = res.getResponseCode();
if (code < 400) return res;
// 4xx (кроме 429) — наша вина: неверный токен или адрес. Повторять бессмысленно.
if (code < 500 && code !== 429) {
throw new Error('Запрос отклонён (' + code + '): ' + res.getContentText());
}
console.warn('Попытка ' + attempt + ' — код ' + code + ', ждём ' + wait + ' мс');
Utilities.sleep(wait + Math.floor(Math.random() * 500));
wait *= 2; // 1с, 2с, 4с, 8с
}
throw new Error('Сервис не ответил за 5 попыток: ' + url);
}
muteHttpExceptions: true отключает автоматическое исключение при коде 500 — иначе мы не успеем прочитать ответ и решить, повторять ли. Случайная добавка к паузе (джиттер) нужна, чтобы десять параллельных запусков не долбили сервис в одну и ту же миллисекунду. И помните про шесть минут: паузы тоже съедают время выполнения, поэтому больше пяти попыток ставить не стоит.
Версии и откат
Плохой день выглядит так: поправили формулу, развернули новую версию, через час выясняется, что скрипт третий час пишет в таблицу мусор. Спасают две вещи.
История изменений. Редактор сам хранит правки: меню «Файл» → «История версий» показывает, каким код был вчера, и позволяет вернуть. Это про исходники.
Развёртывания. Это про то, какой код видят пользователи. «Развернуть» → «Управление развёртываниями»: у каждого свой номер версии. Чтобы откатиться, откройте карандаш и выберите в списке версий предыдущий номер — боевая ссылка /exec тут же начнёт отдавать старый код. Всё это занимает секунд тридцать и не требует ничего восстанавливать из копий.
Отсюда рабочая привычка: не редактируйте боевой код в редакторе «по-быстрому». Каждая правка — это новая версия развёртывания с осмысленным описанием («добавил проверку даты»). Тогда в аварийной ситуации вы не гадаете, что именно сломалось, а просто возвращаете предыдущий номер. Если проект вырос до нескольких человек, следующий шаг — консольная утилита clasp: она умеет выгружать проект в файлы и класть их в Git, а дальше действуют обычные pull request и code review.
Как это работает
У Apps Script нет «сервера, который упал». Каждый запуск — изолированное короткое выполнение на инфраструктуре Google, и единственный след, который от него остаётся, — запись в журнале. Поэтому наблюдаемость приходится закладывать в код: если функция не написала в лог, сколько строк обработала, — узнать это постфактум невозможно.
Исключение, не пойманное вашим try/catch, просто помечает запуск как Failed. Никакого отката: строки, записанные до ошибки, остаются в таблице, письма, ушедшие до ошибки, не вернуть. Поэтому надёжность здесь строится не на транзакциях, а на порядке действий и повторяемости — сначала сделали работу, потом пометили строку «готово»; повторный запуск видит отметку и проходит мимо.
Частые ошибки
- Пустой
catch.catch (e) {}прячет ошибку: скрипт «отработал успешно», данные не записаны. Ловить исключение можно только для того, чтобы что-то с ним сделать. - Письмо на каждую ошибку. Падающий каждые пять минут триггер сжигает суточную почтовую квоту и заваливает вам ящик. Ограничивайте частоту уведомлений.
console.log(объект). В журнале окажется[object Object]. Оборачивайте вJSON.stringify.- Логи в цикле на тысячи итераций. Заметно замедляют выполнение и приближают таймаут.
- Попытка отладить триггер точками останова. Не выйдет: вынесите логику в отдельную функцию и запускайте её из редактора с поддельными данными.
- Правка боевого кода без новой версии. Пользователи по
/execпродолжают работать со старым, вы тестируете новый, и никто не понимает, чей результат в таблице.
Итоги
console.log/console.error— основной инструмент боевой отладки: их видно в журнале выполнений даже для триггеров и веб-запросов.- Точки останова и пошаговый проход работают только для запуска из редактора; логику стоит выносить из
doPostи обработчиков триггеров в обычные функции. - Журнал выполнений показывает тип запуска, статус и длительность — растущая длительность предупреждает о будущем таймауте.
- Обёртка
try/catch+ письмо себе со стеком превращает тихое ночное падение в понятное уведомление; частоту писем ограничивайте черезCacheService. - Сетевые сбои и 429 лечатся повторами с удвоением паузы, а не письмом.
- Откат — это выбор предыдущей версии в «Управлении развёртываниями», а не восстановление кода из памяти.