Basics
Execution log, Logger.log, and debugging
Most Apps Script debugging is printing a value with Logger.log or console.log and reading the error line. This page shows where the output goes and how to trace a failure.
1 of 8
Logger.log and console.log
Logger.log(value) prints one line. It is the old Apps Script way and it still works everywhere. console.log(value) prints the same kind of line and is the way you have probably seen in other JavaScript. On the current runtime, called V8, both are fine and both show up in the same place.
console.log has two extras. It accepts an object or an array directly and shows the contents, while Logger.log turns most things into text first. And it has companions that mark how serious a line is: console.info, console.warn, and console.error. Those severities show up as labels in the Executions history, which makes a real problem easy to pick out of a long run.
Pick one style and stay with it inside a project. Mixed logging is harder to scan than either style on its own.
2 of 8
The panel at the bottom and the Executions page
When you press Run, the Execution log panel opens under your code and fills in as the function goes. It covers this run only, and it is emptied when you run again. It is where you look while you are working.
The Executions page is the history. Click the icon that looks like a list with a clock in the left sidebar. Every run of this project is there, including runs started by triggers while you were asleep, with the function name, the start time, the duration, and a status of Completed, Failed, or Timed out.
Click any row to open the log lines from that run. This is the only way to see what a trigger did, because a trigger run never opens a panel in your editor.
- A run that reached the six minute limit, which is the cap at the time of writing, shows as Timed out, and the log stops mid way.
- Failed rows show the error message next to the row.
- Filter by function name or by status when the list is long.
3 of 8
Read the error line before changing anything
A failed run prints something like: TypeError: Cannot read properties of null (reading "getRange") at updateStatus(Code:12:5). Read it in two halves.
The first half is what went wrong. The second half is where. "at updateStatus" is the function that was running, "Code" is the file Code.gs, "12" is the line number, and "5" is the character on that line. Open the file, go to line 12, and look at that exact line.
When several lines are listed, the top one is where it stopped and the ones below show who called it. Start at the top. The word null in that message is the usual culprit: a sheet, a folder, or a label that was not found, so it came back as null and then you asked it to do something.
4 of 8
Print the value before and after
The fastest way to find a wrong result is to print the value on both sides of the line that changes it. One log before, one log after, then run and compare.
Label every log line with something you can search for, for example "row 5 status before" rather than a bare value. When twenty lines scroll past, unlabelled output tells you nothing.
Print the type as well when a value surprises you. Logger.log(typeof cellValue) identifies primitives such as numbers and strings, but every Date is an object. Check Logger.log(cellValue instanceof Date) for a Date and Array.isArray(cellValue) for an array when a date comparison does not come out true.
5 of 8
Use JSON.stringify to see arrays and objects
A range read with getValues() is a 2D array, meaning an array of rows where each row is an array of cells. Logging it plainly gives you a shape that is hard to read, and an object often prints as the unhelpful text [object Object].
JSON.stringify(value) turns either one into a readable string with the brackets and commas in place, so you can count the rows and see which cells are empty.
function logSheetValues() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const values = sheet.getDataRange().getValues();
Logger.log('Rows read: ' + values.length + '. Columns: ' + values[0].length + '.');
Logger.log('First row: ' + JSON.stringify(values[0]));
Logger.log('All rows: ' + JSON.stringify(values));
} 6 of 8
Catch an error, record it, and pass it on
try and catch let you say what to do when a line fails. Put the risky work in the try block, and the catch block runs with the error instead of the script stopping straight away.
Write a message you will understand later, then use throw error to pass the problem on. Rethrowing matters: a run that swallows its error is marked Completed, so a broken trigger looks healthy in the Executions list and nobody is told.
Notice the check for a missing tab as well. Stopping with a clear message of your own is easier to read than a null error five lines later.
function updateStatusWithLogging() {
try {
writeStatusToFirstRow();
Logger.log('writeStatusToFirstRow finished.');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Logger.log('writeStatusToFirstRow failed: ' + message);
throw error;
}
}
function writeStatusToFirstRow() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Customers');
if (!sheet) {
throw new Error('This spreadsheet has no tab named Customers.');
}
sheet.getRange('B2').setValue('Checked');
} 7 of 8
Failure emails go to the trigger owner
When a trigger run fails, Google emails a summary to the account that created the trigger. It names the project, the function, and the error, and links to the Executions page.
So if a scheduled job stopped working and nobody noticed, check the inbox of the person who installed the trigger, not your own. If a colleague set it up, the mail is sitting with them.
These summaries are gathered up, not instant. Treat them as a signal that something has been failing, and go to the Executions page for the detail.
8 of 8
Test on one row before you loop
Before a loop touches two hundred rows, run the same code on row two only and read the log. If it is wrong, it is wrong once and on data you can check by eye.
The same idea applies to anything that leaves the script. Send the first email to your own address. Write the first result into a spare tab. Log what you are about to delete for one run before you let the delete line do anything.
Widen the range only when the small version is right. Debugging one row is quick, and undoing two hundred is not.
Remember
The short version
- Logger.log and console.log both print to the same place, and console.log also handles objects and severity levels.
- The Execution log panel covers the current run, and the Executions page keeps the history of every run including triggers.
- An error line names the reason, the function, the file, and the line number, so read it before changing code.
- JSON.stringify makes a 2D array or an object readable in the log.
- Catch an error, log a clear message, then rethrow it so a failed run is not recorded as Completed.
Keep going
Put it to work
- Task guide Send an email for each row in a Google Sheet
Use one row per person, then let the script send only the rows that have not already been marked as sent.
- Task guide Run an Apps Script function every day
Run the setup function once, then Apps Script calls the reminder function each day around your chosen hour.
- Task guide Copy unique rows to a clean tab
The script reads your list, keeps the first row for each email address, and rewrites a tab named Clean. The source tab is never changed.
- Basics Run your first Apps Script function
Open Extensions > Apps Script from a Google Sheet or Doc to reach the code editor. Replace the starter function myFunction() { }, save with Ctrl+S, press Run, and read what Logger.log printed.
- Basics Common Apps Script errors and what they mean
Apps Script error messages name the failure directly, from TypeError: Cannot read properties of null (reading 'getRange') to ReferenceError and quota limits. Find the exact message below for the cause and fix.
- Basics Google Apps Script triggers explained
A Google Apps Script trigger runs a function when an event happens, without pressing Run. Simple triggers like onEdit and onOpen need no setup, while installable triggers run on a schedule or form submit using the OAuth scopes approved by their owner.