Skip to content

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.

Show the shape of the data you read
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.

A wrapper that records the failure and rethrows it
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

All basics