Apps Script Error Handling and Debugging: Fix Broken Scripts Like a Pro
Master Google Apps Script error handling and debugging. Learn try/catch blocks, Logger.log vs console.log, the Execution Transcript, breakpoints, common error types, and how to log errors to Sheets and Cloud Logging.
Every Apps Script developer has been there. You write a script that works perfectly on the first run, set up a trigger, and come back the next morning to find it has silently failed twenty-three times overnight. No email alert. No obvious error. Just a spreadsheet that has not been updated and a vague sense that something is wrong.
The difference between a brittle script and a reliable one is not complexity. It is error handling. Scripts that anticipate failures, catch them gracefully, report them clearly, and recover where possible are the ones that earn trust. Scripts that assume nothing will ever go wrong are the ones that get deleted after a bad Monday.
This guide covers the complete error handling and debugging toolkit for Apps Script: logging strategies, the built-in debugger, the most common error types you will encounter, how to notify yourself when things go wrong, and how to build a systematic debugging workflow that gets you from "the script broke" to "fixed and re-deployed" as quickly as possible.
Logger.log() vs console.log(): Which One to Use
Apps Script gives you two primary logging methods, and understanding when to use each one saves a lot of confusion.
Logger.log()
Logger.log() writes output to the Apps Script Logs panel, which you access via View > Logs in the editor, or via the keyboard shortcut Ctrl+Enter. Logs accumulate during a single execution and are displayed after the run completes. They are cleared when the next execution starts.
function exampleLogging() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastRow = sheet.getLastRow();
Logger.log('Script started. Last row with data: %s', lastRow);
for (var i = 2; i <= lastRow; i++) {
var value = sheet.getRange(i, 1).getValue();
Logger.log('Row %s: %s', i, value);
}
Logger.log('Script complete.');
}
Logger.log() supports printf-style formatting using %s for strings, %d for numbers, and %j for JSON objects. This is cleaner than string concatenation for complex log messages.
When to use Logger.log(): Interactive development and manual testing. When you run a function by hand and want to see what values are being read or calculated, Logger.log() is the quickest feedback loop.
console.log()
console.log() writes output to the Executions panel (the page at script.google.com/home/executions), not the Logs panel. Importantly, when your script is connected to a Google Cloud project, console.log() output is also written to Cloud Logging, where it persists and can be queried long after the execution completes.
function exampleConsoleLogging() {
console.log('Processing started at: ' + new Date().toISOString());
try {
// some operation
console.info('Operation succeeded');
} catch (e) {
console.error('Operation failed: ' + e.message);
}
}
console in Apps Script supports the same levels as in a browser: console.log(), console.info(), console.warn(), and console.error(). These severity levels are meaningful in Cloud Logging, where you can filter and alert on ERROR entries specifically.
When to use console.log(): Trigger-based scripts that run unattended. When your script fires on a schedule or in response to a form submission, no one is watching the Logs panel. console.log() writes to the Executions panel, which you can review later, and to Cloud Logging if connected.
The practical rule: Use Logger.log() while building and testing. Use console.log() (or both) in production scripts. Use console.error() specifically for exceptions and failure states so they stand out in Cloud Logging.
The Execution Transcript
The Execution Transcript is one of the most overlooked debugging tools in Apps Script. It is available in the editor at the bottom of the screen after a run, and shows a timestamped record of every service call the script made, not just your log output.
To view it:
- Run any function in the Apps Script editor.
- At the bottom of the editor, click the Execution log tab (it appears after the run starts).
- For historical executions, open script.google.com/home/executions and select any past execution to see its full transcript.
A typical transcript entry looks like this:
[19-02-2026 08:14:23:412 AEST] Starting execution
[19-02-2026 08:14:23:501 AEST] SpreadsheetApp.getActiveSpreadsheet() [0.017 seconds]
[19-02-2026 08:14:23:612 AEST] Sheet.getLastRow() [0.023 seconds]
[19-02-2026 08:14:24:887 AEST] Sheet.getRange([2, 1, 1, 3]) [0.041 seconds]
[19-02-2026 08:14:25:109 AEST] Range.setBackground([#f4cccc]) [0.089 seconds]
[19-02-2026 08:14:26:001 AEST] Execution succeeded [2.588 seconds total]
This is invaluable for two reasons:
- Performance profiling: The timestamps show you exactly which service calls are slow. A script that takes twenty seconds when it should take two usually has a
getRange()orsetValue()call inside a loop. The transcript shows you exactly where the time is going. - Failure location: When a script throws an error, the transcript shows you the last service call that completed before the failure, which is often more useful than the line number in the error message.
Infographic placeholder: A labelled diagram of the Apps Script editor interface highlighting: (1) the Run button, (2) the function selector dropdown, (3) the Execution log tab, (4) the Logs panel, and (5) the Debugger toolbar — to appear alongside this section.
try/catch Blocks: The Foundation of Error Handling
A try/catch block is the core construct for handling errors in JavaScript and Apps Script. Code inside the try block runs normally. If an exception is thrown, execution jumps to the catch block, preventing the error from propagating up and terminating the entire script.
Basic try/catch
function updateClientRecord(rowNumber, newStatus) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
try {
var range = sheet.getRange(rowNumber, 3);
range.setValue(newStatus);
Logger.log('Updated row %s to status: %s', rowNumber, newStatus);
} catch (e) {
Logger.log('ERROR updating row %s: %s', rowNumber, e.message);
// Script continues rather than crashing here
}
}
Without the try/catch, a single bad row number or a permissions issue would crash the entire function. With it, the error is captured and logged, and any subsequent operations continue.
The Error Object
The e parameter in catch (e) is a JavaScript Error object with several useful properties:
function demonstrateErrorObject() {
try {
// Deliberately cause an error
var x = null;
var y = x.someProperty; // TypeError: Cannot read properties of null
} catch (e) {
Logger.log('Message: ' + e.message); // "Cannot read properties of null (reading 'someProperty')"
Logger.log('Name: ' + e.name); // "TypeError"
Logger.log('Stack: ' + e.stack); // Full stack trace with line numbers
}
}
Always log e.message at a minimum. Log e.stack when debugging complex multi-function scripts, as the stack trace shows you the full chain of function calls that led to the error.
try/catch/finally
The finally block runs regardless of whether an exception was thrown, making it useful for cleanup operations:
function processWithCleanup() {
var lock = LockService.getScriptLock();
try {
lock.waitLock(10000); // Wait up to 10 seconds for the lock
// ... do the work ...
console.log('Processing complete');
} catch (e) {
console.error('Processing failed: ' + e.message);
throw e; // Re-throw so the trigger marks this execution as failed
} finally {
lock.releaseLock(); // Always release the lock, success or failure
}
}
Re-throwing the error after logging it (throw e) is an important pattern. It lets you log the error for your own visibility while still allowing Apps Script's trigger system to mark the execution as failed -- which is what you want when something genuinely broke.
Custom Error Messages
JavaScript lets you throw your own errors with descriptive messages, which makes debugging much faster when the message actually explains the business context of the failure.
function getActiveClient(clientId) {
if (!clientId) {
throw new Error('getActiveClient: clientId is required but was not provided.');
}
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Clients');
if (!sheet) {
throw new Error('getActiveClient: Sheet named "Clients" was not found. Check the tab name.');
}
var data = sheet.getDataRange().getValues();
var headers = data[0];
var idColumn = headers.indexOf('Client ID');
if (idColumn === -1) {
throw new Error('getActiveClient: Column "Client ID" not found in the Clients sheet. Headers found: ' + headers.join(', '));
}
for (var i = 1; i < data.length; i++) {
if (data[i][idColumn] === clientId) {
return data[i];
}
}
throw new Error('getActiveClient: No client found with ID "' + clientId + '". Check the Clients sheet for this value.');
}
Custom error messages that include the function name, the expected condition, and the actual value found are dramatically easier to act on than a generic "TypeError at line 47".
Debugging in the Apps Script Editor: Breakpoints and Stepping
The Apps Script editor has a built-in debugger that lets you pause execution at any line and inspect the values of all variables. This is faster than adding Logger.log() calls everywhere when you are trying to understand what a script is doing at a specific point.
Setting Breakpoints
- Open your script in the Apps Script editor.
- Click on the line number in the left margin of any line you want to pause at. A red dot appears, indicating a breakpoint.
- Click the Debug button (the bug icon next to the Run button) instead of Run.
The script will execute until it reaches the breakpoint, then pause.
Stepping Through Code
Once paused at a breakpoint, the debugger toolbar appears at the top of the editor:
- Step over (F10): Execute the current line and move to the next one. Function calls are executed as a single step without entering them.
- Step into (F11): If the current line calls a function, enter that function and pause at its first line.
- Step out (Shift+F11): Finish executing the current function and return to the caller.
- Resume (F8): Continue execution until the next breakpoint or the end of the script.
Inspecting Variables
While paused, the Variables panel on the left side of the editor shows all variables currently in scope with their current values. For complex objects and arrays, you can expand the tree to see nested properties.
You can also hover over any variable in the code editor while paused to see its current value in a tooltip.
Image placeholder: Screenshot of the Apps Script debugger paused at a breakpoint, with the Variables panel open showing a spreadsheet range object and its values — to appear alongside the breakpoints section.
Common Errors and How to Fix Them
TypeError: Cannot Read Properties of Undefined
This is the most common error in Apps Script. It almost always means you are trying to access a property or method on a value that is null or undefined.
// BAD: Will crash if getSheetByName returns null
function badExample() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Report');
var value = sheet.getRange('A1').getValue(); // TypeError if sheet is null
}
// GOOD: Check before accessing
function goodExample() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Report');
if (!sheet) {
throw new Error('Sheet "Report" not found. Check the tab name matches exactly (case-sensitive).');
}
var value = sheet.getRange('A1').getValue();
}
Common causes: A sheet tab has been renamed, a column header has changed, a cell you expected to contain data is empty, or getValues() returned fewer rows than expected.
RangeError: Range Not Found
This occurs when you specify a cell or range that does not exist in the sheet, usually due to an out-of-bounds row or column number.
// BAD: getLastRow() returns 0 for an empty sheet, making getRange(0, 1) invalid
function badRangeExample() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastRow = sheet.getLastRow();
var data = sheet.getRange(1, 1, lastRow, 3).getValues(); // RangeError if lastRow is 0
}
// GOOD: Guard against empty sheets
function goodRangeExample() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastRow = sheet.getLastRow();
if (lastRow < 2) {
Logger.log('No data rows found (only header or completely empty). Exiting.');
return;
}
// Start from row 2 (skip header), read 3 columns
var data = sheet.getRange(2, 1, lastRow - 1, 3).getValues();
Logger.log('Read %s rows of data.', data.length);
}
Permission Errors (Exception: You do not have permission)
Permission errors occur when a script tries to access a resource it has not been authorised to use, or when the authorising user does not have access to the resource.
Common scenarios:
- The script is trying to send email (
MailApp.sendEmail()) but was only authorised for Sheets access. Re-run the script manually to trigger a new authorisation dialog. - The script references a Google Drive folder by ID that the running user does not have access to.
- A time-driven trigger is running as a user who has left the organisation, and their account no longer has permission to access the shared drive.
- The script is trying to access an API that has not been enabled in the Google Cloud project settings.
Resolution: Re-run the script manually, complete the updated authorisation flow, and verify that the account running the trigger (visible in the Triggers panel) has the necessary permissions on all resources the script accesses.
Quota Exceeded (Exception: Service invoked too many times)
Apps Script imposes daily quotas on most service calls. Common limits for Google Workspace accounts include:
- Email sends (MailApp): 1,500 per day (Workspace), 100 per day (free Gmail)
- UrlFetchApp calls: 20,000 per day
- Spreadsheet reads/writes: No hard call limit, but total script runtime is capped at 6 minutes per execution (30 minutes for Workspace Business and above)
- Calendar event creation: 5,000 per day
When you hit a quota, the error message is clear: Exception: Service invoked too many times for one day: email.
Strategies to avoid quota errors:
// BAD: Sends an email for every single row — burns through quota fast
function sendEmailPerRow() {
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
MailApp.sendEmail(data[i][0], 'Update', 'Your record has been updated.');
}
}
// GOOD: Batch all recipients into a single digest email
function sendBatchDigest() {
var data = sheet.getDataRange().getValues();
var recipients = [];
for (var i = 1; i < data.length; i++) {
recipients.push(data[i][0]);
}
var body = 'The following records have been updated:\n' + recipients.join('\n');
MailApp.sendEmail('admin@yourdomain.com.au', 'Daily Update Digest', body);
}
Check current quota limits in your project at script.google.com/home/usersettings under Quotas.
Error Notification Emails
When a script runs on a trigger, no one is watching the execution. Error notification emails are how you find out something broke before a user does.
Built-In Trigger Failure Notifications
The simplest approach: Apps Script will automatically email the script owner when a trigger-based execution fails due to an unhandled exception.
To enable this:
- Open the Triggers panel (clock icon in the left sidebar).
- Click the three-dot menu next to any trigger and select Edit trigger.
- Under Failure notification settings, choose Notify me immediately.
This covers unhandled exceptions but not logic errors -- a script that runs to completion but processes data incorrectly will not trigger a failure notification.
Custom Error Notification Function
For more control, build a dedicated notification function and call it from your error handling blocks:
/**
* Sends an error notification email to a nominated address.
* Call this from catch blocks in production scripts.
*
* @param {string} scriptName - Human-readable name of the script that failed.
* @param {string} functionName - The function where the error occurred.
* @param {Error} error - The caught Error object.
*/
function notifyOnError(scriptName, functionName, error) {
var NOTIFICATION_EMAIL = 'your.email@yourdomain.com.au';
var timestamp = Utilities.formatDate(
new Date(),
Session.getScriptTimeZone(),
'dd/MM/yyyy HH:mm:ss zzz'
);
var subject = '[ACTION REQUIRED] Apps Script Error: ' + scriptName;
var body = [
'An error occurred in your Apps Script automation.',
'',
'Script: ' + scriptName,
'Function: ' + functionName,
'Time: ' + timestamp,
'Error: ' + error.message,
'',
'Stack trace:',
error.stack,
'',
'Review the full execution log at:',
'https://script.google.com/home/executions'
].join('\n');
try {
MailApp.sendEmail(NOTIFICATION_EMAIL, subject, body);
} catch (mailError) {
// If email itself fails (e.g., quota exceeded), log it but don't create an infinite loop
console.error('notifyOnError: Failed to send notification email: ' + mailError.message);
}
}
// Usage in a production script:
function myProductionFunction() {
try {
// ... your logic ...
} catch (e) {
console.error('myProductionFunction failed: ' + e.message);
notifyOnError('Invoice Reminder Script', 'myProductionFunction', e);
throw e; // Re-throw to mark the trigger execution as failed
}
}
Logging Errors to a Google Sheet
For scripts that process many rows or items, a simple boolean "success or failure" email is not enough. You need a permanent audit trail: what was processed, when, and what happened. Logging errors to a dedicated Sheet gives you that.
/**
* Appends a log entry to a dedicated "Script Log" sheet.
* Creates the sheet and headers if it does not already exist.
*
* @param {string} level - 'INFO', 'WARNING', or 'ERROR'
* @param {string} message - Human-readable description of the event.
* @param {string} [details] - Optional additional context (e.g., row number, file name).
*/
function logToSheet(level, message, details) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var logSheet = ss.getSheetByName('Script Log');
// Create the log sheet if it does not exist
if (!logSheet) {
logSheet = ss.insertSheet('Script Log');
logSheet.appendRow(['Timestamp', 'Level', 'Message', 'Details']);
logSheet.getRange('1:1').setFontWeight('bold');
logSheet.setFrozenRows(1);
}
var timestamp = Utilities.formatDate(
new Date(),
Session.getScriptTimeZone(),
'dd/MM/yyyy HH:mm:ss'
);
logSheet.appendRow([timestamp, level, message, details || '']);
// Colour-code the level column for readability
var lastRow = logSheet.getLastRow();
var levelCell = logSheet.getRange(lastRow, 2);
if (level === 'ERROR') {
levelCell.setBackground('#f4cccc'); // Light red
} else if (level === 'WARNING') {
levelCell.setBackground('#fff2cc'); // Light yellow
} else {
levelCell.setBackground('#d9ead3'); // Light green
}
}
// Example usage in a batch processing function:
function processBatchOrders() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Orders');
var data = sheet.getDataRange().getValues();
logToSheet('INFO', 'Batch processing started', data.length - 1 + ' orders to process');
var successCount = 0;
var errorCount = 0;
for (var i = 1; i < data.length; i++) {
var orderId = data[i][0];
try {
// ... process the order ...
successCount++;
} catch (e) {
errorCount++;
logToSheet('ERROR', 'Failed to process order', 'Order ID: ' + orderId + ' | ' + e.message);
}
}
logToSheet(
errorCount > 0 ? 'WARNING' : 'INFO',
'Batch processing complete',
'Success: ' + successCount + ', Errors: ' + errorCount
);
}
A Sheet log is particularly useful for non-technical stakeholders who can open the spreadsheet and see what happened without needing access to the Apps Script editor.
Practical Debugging Workflow
When a script is broken and you need to find and fix the problem efficiently, follow this sequence:
1. Read the error message in full. The Executions panel at script.google.com/home/executions shows the error message and the line number. This is your starting point. Do not skip it.
2. Check the Execution Transcript. The transcript shows which service call failed and the state of execution immediately before the failure. Often the root cause is visible here before you even open the code.
3. Reproduce the error manually. If the script failed on a trigger, run the relevant function manually with the same conditions. A manually triggered execution is easier to debug because you can see the Logs panel in real time.
4. Add targeted Logger.log() calls. Rather than stepping through the entire script, identify the section you think is failing and add Logger.log() calls to print the values of key variables immediately before the error line.
// Before debugging:
function processRow(i) {
var value = sheet.getRange(i, 2).getValue();
var result = transformValue(value);
sheet.getRange(i, 4).setValue(result);
}
// After adding targeted logging:
function processRow(i) {
var value = sheet.getRange(i, 2).getValue();
Logger.log('Row %s: raw value = %s (type: %s)', i, value, typeof value);
var result = transformValue(value);
Logger.log('Row %s: transformed result = %s', i, result);
sheet.getRange(i, 4).setValue(result);
}
5. Use the debugger for complex state. If the values look correct in the logs but the script still fails, set a breakpoint at the failing line and use the Variables panel to inspect all in-scope values. This is faster than adding more log calls.
6. Isolate the failing case. If the script processes a hundred rows and fails on row 47, extract the data for that row and call your function with that specific input. Reproduce the minimum case that triggers the bug.
7. Fix, test, remove debug logging, re-deploy. Once the fix is confirmed, remove the debug Logger.log() calls you added (or replace them with console.log() at appropriate severity levels) and run the script a final time to confirm the clean version works.
Using Stackdriver / Cloud Logging
When your Apps Script project is linked to a Google Cloud project (see the Apps Script Cloud Projects guide for setup steps), console.log(), console.warn(), and console.error() output flows into Cloud Logging automatically. This transforms Apps Script from something you debug reactively into something you can monitor proactively.
Viewing Logs in Cloud Console
- Open console.cloud.google.com and navigate to Logging > Log Explorer.
- In the query editor, filter to your Apps Script logs:
resource.type="app_script_function"
- Use the severity filter to show only errors:
resource.type="app_script_function"
severity>=ERROR
- Filter by function name if you have multiple scripts in the same project:
resource.type="app_script_function"
labels."script.googleapis.com/function_name"="processBatchOrders"
severity>=WARNING
Setting Up Log-Based Alerts
The real value of Cloud Logging is alert automation. Rather than checking the log explorer manually, configure an alert to notify you the moment an error occurs:
- In Cloud Console, navigate to Logging > Log-based Alerts.
- Click Create alert.
- Set the filter to
resource.type="app_script_function" AND severity>=ERROR. - Set the notification channel to your email address or a Google Chat webhook.
- Set minimum time between notifications to avoid alert floods (5 to 15 minutes is typical).
With this in place, you receive a notification within minutes of any unhandled exception in any Apps Script function linked to that Cloud project. For Australian businesses running critical automations (invoice processing, client onboarding, payroll data exports), this level of observability is worth the ten-minute setup.
Infographic placeholder: A flow diagram showing the error signal chain: Apps Script exception → console.error() → Cloud Logging → Log-based Alert → email/Chat notification → developer response — to appear in this section.
Affiliate & Partner Programs
The tools below are directly relevant to the Apps Script error handling and monitoring workflows covered in this guide. Some links are referral links that may provide benefits to you and to CloudGeeks. We only recommend what we actively use with clients.
Google Workspace — Apps Script is included in every Google Workspace plan at no additional cost. Business Standard and above increase the maximum script runtime from 6 minutes to 30 minutes per execution, which is significant for batch-processing scripts that handle large data sets. Workspace Business plans also include better integration with Google Cloud Logging for production monitoring.
Start or upgrade your Google Workspace plan
Google Cloud Platform — Cloud Logging, used for Stackdriver-level Apps Script monitoring described in this guide, is part of Google Cloud. The first 50 GiB of log ingestion per month is free, which covers the output of any reasonable Apps Script deployment. Log-based alerts are also free to configure.
Start with a free trial at console.cloud.google.com.
Conclusion
A broken script is not a failure -- it is information. Every TypeError, every quota error, and every permission exception is telling you something specific about the gap between what your script assumes and what the real world delivers. The goal of error handling is not to hide those gaps but to surface them clearly, quickly, and in enough detail that you can act on them.
The practical takeaway from this guide is a layered approach: use Logger.log() while building, console.error() in production, try/catch around any operation that can fail, custom error messages that describe the business context, Sheet-based logging for audit trails, and Cloud Logging alerts for unattended trigger-based scripts. Each layer adds resilience.
Start with whichever layer your current scripts are missing. If you have no error handling at all, add try/catch blocks to your most critical functions today. If you have try/catch but no notifications, add the notifyOnError pattern. If you have notifications but no Cloud Logging, link your project to GCP and turn on log-based alerts.
Reliable automation is not about writing perfect scripts on the first attempt. It is about building scripts that tell you, loudly and specifically, when something goes wrong.
Need help with your Google Workspace setup? Contact our team for a free consultation.
Ash Ganda is the founder of CloudGeeks, a Google Workspace consultancy helping Australian SMBs get more from their cloud tools. He writes about practical technology strategies at insights.cloudgeeks.com.au.