HomeAbout MeBook a Call

Robust Apps Script Logging and Error Handling for AI Agents

By Vo Tu Duc
May 05, 2026
Robust Apps Script Logging and Error Handling for AI Agents

From automatically creating jobs from an email to managing your entire Google Drive, AI is transforming workflows. But this powerful automation comes with high stakes you need to understand.

image 0

The High Stakes of AI [Automated Job Creation in Real Time Jobber and Google Sheets Integration from Gmail](https://votuduc.com/Automated-Job-Creation-in-Jobber-from-Gmail-p115606) in [Automatically create new folders in Google Drive, generate templates in new folders, fill out text automatically in new files, and save info in [Automated Web Scraping with [Multilingual Text-to-Speech Tool with SocialSheet Streamline Your Social Media Posting 123](https://votuduc.com/Multilingual-Text-to-Speech-Tool-with-Google-Workspace-p809282)](https://votuduc.com/Automated-Web-Scraping-with-Google-Sheets-p292968)](https://workspace.google.com/marketplace/app/auto_create_folder_and_files/430076014869)

Integrating AI into AC2F Streamline Your Google Drive Workflow isn’t just about writing a clever script that calls the Gemini API. It’s about deploying a production system that people rely on. When your AI agent is responsible for summarizing executive reports, triaging customer support emails, or generating financial projections in Google Sheets, the stakes are incredibly high. The line between a “cool prototype” and a “mission-critical tool” is crossed the moment real business processes depend on its output. This is where the conversation must shift from “does it work?” to “how does it fail, and how will we know?”

Why Silent Failures are the Enemy of Production Systems

A script that throws a big, red, ugly exception is a good script. It failed, and it had the decency to tell you. You get an email notification, you see the failed execution in your dashboard, and you can immediately start debugging. The real monster in the closet is the silent failure.

image 1

A silent failure is an error condition that doesn’t halt execution. It’s the subtle bug that leads to incorrect, incomplete, or nonsensical results without raising an alarm. For an AI agent, this could look like:

  • Graceful Degradation Gone Wrong: An API call to your LLM times out. Your script’s try...catch block handles it, but instead of logging a critical error, it just inserts a placeholder like "AI summary not available" into a client-facing report. The workflow completed “successfully,” but the output is useless or even damaging.

  • Misinterpreted API Responses: The AI model returns a valid JSON structure, but its content indicates a refusal to answer or a misunderstanding of the prompt. Your code, only checking for a 200 OK status and valid JSON, happily processes the garbage data.

  • Logical Gaps: A change in an input document’s format causes your parsing logic to skip a critical section. No error is thrown; the data is simply missing from the final output.

These silent failures are insidious. They erode trust over time. When a manager realizes the weekly sales summary generated by your script has been missing data from the West Coast office for three weeks, they don’t just lose faith in the Automated Quote Generation and Delivery System for Jobber; they lose faith in your team’s ability to deliver reliable tools. A loud failure is a problem to be fixed; a silent failure is a breach of trust.

Moving Beyond Logger.log: The Limits of Basic Debugging

Every Apps Script developer starts with Logger.log(). It’s the trusty flashlight we use to peer into the dark corners of our code during development. It’s simple, effective, and perfect for checking a variable’s value or confirming a code path was executed. But relying on Logger.log() for a production AI agent is like using that same flashlight to perform surgery. It’s the wrong tool for the job, and the results will be messy.

The limitations become painfully obvious once your script is running on a trigger, processing real data for real users:

  • It’s Ephemeral: Logger output is temporary and has size limits. When a user reports an issue that happened yesterday, your logs are likely already gone. You have no history, no audit trail.

  • It’s Unstructured: The log is a flat wall of text. You can’t easily filter for all errors, search for logs related to a specific Document ID, or analyze performance trends. It’s a nightmare to parse when you’re trying to diagnose a complex, multi-step process.

  • It Lacks Severity: A log entry saying “Processing item 5” looks identical to “FATAL: Failed to connect to external API.” There’s no concept of levels like INFO, WARN, or ERROR, making it impossible to quickly identify critical issues in a sea of routine messages.

  • It Lacks Context: A log entry doesn’t automatically tell you which user triggered the script, the execution ID, or the function name. You have to manually add this information to every single log statement, which is both tedious and easy to forget.

For a robust system, you need a logging framework that persists, is structured, and provides context. Logger.log() is for your development sandbox; production systems demand a professional solution.

The Business Impact of Unreliable AI-Powered Workflows

When an AI-powered workflow fails, it’s not just a technical problem; it’s a business problem. The consequences ripple outward, affecting efficiency, finances, and reputation.

  • Operational Drag: The promise of AI is to automate tedious work and free up human capital for higher-value tasks. An unreliable agent does the opposite. If employees have to constantly double-check the AI’s output or manually correct its mistakes, you haven’t created an Automated Work Order Processing for UPS—you’ve created a new, frustrating form of manual labor. The time spent “babysitting the bot” can easily negate any efficiency gains.

  • Costly Errors: Imagine an agent designed to scan incoming invoices, extract line items, and stage them for payment in a Google Sheet. A subtle parsing error could lead to overpayments, underpayments, or missed payment deadlines, resulting in real financial losses and damaged vendor relationships. Or consider an AI that categorizes customer support tickets; miscategorization can delay responses to critical issues, leading to customer churn.

  • Erosion of Strategic Trust: Perhaps the most significant impact is the damage to trust. When you champion an AI initiative, you are asking the business to have faith in a new way of working. If your first major project is plagued by un-debuggable errors and unreliable outputs, stakeholders will lose confidence. Future projects will face greater scrutiny, securing budget will become harder, and the entire strategic goal of leveraging AI within the organization could be jeopardized. A single poorly implemented agent can poison the well for years to come.

Architecting the Automation Monitor: A Pragmatic Approach

Before we can handle errors, we need to see them. Before we can optimize a process, we need to measure it. The foundation of any robust automation system isn’t just clever code; it’s a clear, accessible, and centralized record of its actions. This is our “mission control,” our single source of truth.

We’re not going to over-engineer this. Forget complex log-shipping agents or expensive third-party observability platforms for now. We’re going to build a powerful and surprisingly flexible monitoring system using a tool you already know and love (or at least, know): Google Sheets. It’s pragmatic, effective, and perfectly suited for the scale of most Apps Script projects.

Why Google Sheets is a Powerful Tool for Centralized Logging

I know what you might be thinking. “Google Sheets? For application logs? Isn’t that… unprofessional?”

It’s a fair question. In the world of enterprise software, developers use dedicated services like Datadog, Sentry, or Google’s own Cloud Logging. And for good reason—those platforms are built for massive scale. But for our AI agents and internal automations running on Apps Script, using Google Sheets isn’t a compromise; it’s a strategic advantage.

Here’s why:

  • Zero Barrier to Entry: It’s free and already part of your Automated Client Onboarding with Google Forms and Google Drive.. There’s no new service to provision, no budget to approve, and no new UI to learn. You can have a logging system up and running in minutes.

  • Ultimate Accessibility & Familiarity: Anyone on your team can open the log sheet and understand what’s happening. There’s no need to learn a complex query language. You can filter, sort, and search using the same skills you use to manage a project tracker. This democratizes observability.

  • Powerful Built-in Visualization and Analysis: A spreadsheet is more than just a grid of data. It’s a data analysis powerhouse in disguise.

  • Conditional Formatting: Instantly highlight rows with an ERROR status in red and SUCCESS in green. A quick glance at the sheet gives you an immediate health check of your entire system.

  • Charts & Pivot Tables: Want to see your error rate over the last 30 days? Or which script is running most often? You can build a simple dashboard directly within your log sheet in minutes.

  • Built-in Notifications: Set up a notification rule to email you instantly whenever a new row is added that contains the word “CRITICAL”. You get real-time alerting without writing a single extra line of code.

  • Programmatic Control: As an Apps Script developer, the entire Google Sheets interface is your playground. You have full, native API access to write new log entries, clear old ones, or even use a script to generate daily summary reports.

Of course, it’s not a silver bullet. If your scripts generate tens of thousands of log entries per day, you will eventually hit the cell limits of Google Sheets and performance will degrade. For that kind of hyper-scale operation, a dedicated logging service is the right tool. But for the vast majority of automations—from daily report generators to AI-powered document processors—Google Sheets is the perfect, pragmatic choice.

Designing the Log Schema: Timestamp, Status, Context, and More

A log is useless if it’s an unstructured mess. To make our Google Sheet monitor effective, we need a consistent structure—a schema. Every time our script writes a log entry, it will populate a new row with a predictable set of columns. This discipline is what allows for effective sorting, filtering, and analysis later.

Here is a robust, all-purpose schema that will serve you well for nearly any project.

| Column Header | Data Type | Description | Example |

| :--- | :--- | :--- | :--- |

| Timestamp | DateTime | (Required) The exact moment the event occurred. Use a full ISO 8601 format for unambiguous, sortable timestamps. | 2023-10-27T10:30:05.123Z |

| Status | String | (Required) A single, capitalized word describing the event type. Use a controlled vocabulary. | INFO, SUCCESS, WARNING, ERROR |

| ScriptName | String | The name of the Apps Script project file (.gs) that generated the log. Crucial when multiple scripts log to one sheet. | GmailProcessor.gs |

| FunctionName | String | The specific function within the script that was executing. This helps you pinpoint the exact source of an event. | processNewInvoices |

| Message | String | A concise, human-readable description of the event. This is the “what happened.” | Successfully processed 3 new invoices. |

| Context | String (JSON) | A flexible field for structured data. For errors, this holds the error stack. For successes, it can hold metrics like IDs processed or execution time. | {"invoice_ids":[12,45,98],"duration_ms":1543} |

Why this schema works:

  • Timestamp and Status are your primary sorting and filtering keys. “Show me all ERROR events from the last 24 hours” becomes a simple sheet filter.

  • ScriptName and FunctionName provide the “where.” They tell you exactly which part of your codebase is responsible for the log entry, dramatically speeding up debugging.

  • Message is the “what” for humans. It’s the quick summary you read when scanning the logs.

  • Context is the “why” and “how” for developers. Storing rich data like error objects, API responses, or performance metrics as a JSON string in this single cell is a game-changer. It keeps your log sheet tidy while providing deep diagnostic information when you need it.

Building a Reusable logToSheet Function in Apps Script

Now, let’s turn our schema into code. The goal is to create a single, reusable function that any other part of our project can call to write a log entry. This adheres to the DRY (Don’t Repeat Yourself) principle and ensures every log entry is perfectly formatted.

We’ll place this function in a dedicated Logger.gs file within our Apps Script project to keep things organized.

First, let’s define some constants to avoid hardcoding IDs and names directly in our function logic.


// File: Config.gs

// The ID of the Google Sheet you created for logging.

const LOG_SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID_GOES_HERE';

// The name of the specific sheet (tab) within that spreadsheet.

const LOG_SHEET_NAME = 'Automation_Logs';

Now, for the core logging function. This code is designed to be robust. It dynamically determines the script and function name, handles different kinds of context data, and even includes its own try...catch block so that a failure in the logging mechanism doesn’t crash your main script.


// File: Logger.gs

/**

* Appends a structured log entry to the designated Google Sheet.

*

* @param {string} status The status of the log (e.g., 'INFO', 'SUCCESS', 'ERROR').

* @param {string} message A human-readable description of the event.

* @param {object | string} [context={}] Optional. An object or string containing additional data for debugging.

*/

function logToSheet(status, message, context = {}) {

try {

const ss = SpreadsheetApp.openById(LOG_SPREADSHEET_ID);

const sheet = ss.getSheetByName(LOG_SHEET_NAME);

// If the sheet doesn't exist, we can't log.

if (!sheet) {

console.error(`Logging sheet named "${LOG_SHEET_NAME}" not found.`);

return;

}

// A little trick to get the caller function's name for context.

// This is not 100% reliable in all V8 runtime scenarios but works well for simple cases.

const functionName = (new Error()).stack.split('\n')[2].trim().split(' ')[1] || 'unknown';

// The script name is simply the name of the active script project.

const scriptName = ScriptApp.getScriptId(); // Or a custom name if you prefer.

// Ensure context is always a string for the sheet cell.

const contextString = typeof context === 'object' ? JSON.stringify(context) : context;

const timestamp = new Date();

// Append the row according to our defined schema.

sheet.appendRow([

timestamp,

status.toUpperCase(),

scriptName,

functionName,

message,

contextString

]);

} catch (e) {

// Failsafe: If logging to the sheet fails, log the error to the built-in Apps Script logger.

console.error(`Failed to write to log sheet. Original message: [${status}] ${message}. Error: ${e.message}`);

}

}

How to Use Your New Logger

Using this function throughout your code is incredibly simple.

Example 1: Logging a successful operation


function processUserData() {

const usersProcessed = 150;

const timeTaken = 3450; // in milliseconds

// ... your logic here ...

logToSheet('SUCCESS', `Processed ${usersProcessed} user records.`, {

count: usersProcessed,

duration_ms: timeTaken

});

}

Example 2: Logging an error within a try...catch block


function callExternalAPI() {

try {

const response = UrlFetchApp.fetch('https://api.example.com/data');

// ... process the response ...

logToSheet('INFO', 'Successfully fetched data from external API.');

} catch (e) {

// This is the most important use case!

logToSheet('ERROR', 'Failed to fetch data from external API.', {

errorMessage: e.message,

stackTrace: e.stack,

url: 'https://api.example.com/data'

});

// You might also want to re-throw the error or handle it further.

}

}

With this single, reusable function and a well-designed Google Sheet, you have just built a robust, scalable, and highly accessible monitoring system for all your automations.

The Bulletproof Try-Catch Pattern for Gemini API Calls

Making an external API call is an act of faith. You send a request into the void and trust that a well-formed response will return. But in the real world of network hiccups, rate limits, and malformed inputs, faith isn’t a strategy. For mission-critical operations, especially with AI agents, you need a robust pattern that anticipates failure and handles it gracefully.

This is where the standard try...catch block, supercharged for the nuances of AI Powered Cover Letter Automation Engine’s UrlFetchApp, becomes your most valuable asset. We’re not just catching errors; we’re interrogating them, logging them with rich context, and ensuring our application can recover or fail intelligently.

Structuring the Core API Request within a Try Block

The try block is your optimistic path—it’s where you define the code that you expect to work. For a Gemini API call, this involves constructing a request using UrlFetchApp.fetch(). The real magic, however, lies in one specific parameter: muteHttpExceptions.

Setting muteHttpExceptions to true is the cornerstone of this entire pattern. By default, if UrlFetchApp receives an HTTP error (like a 400 Bad Request or 503 Service Unavailable), it throws an exception and halts your script. This is not what we want. We want the chance to inspect that error response, parse its contents, and log the specific reason for the failure. With muteHttpExceptions: true, UrlFetchApp will return the full HTTP response object, error or not, allowing our code to decide what to do next.

Here’s how to structure the core request:


function callGeminiAPI(prompt) {

const API_KEY = 'YOUR_GEMINI_API_KEY';

const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`;

// The 'optimistic' path lives inside the try block

try {

const requestBody = {

"contents": [{

"parts": [{

"text": prompt

}]

}]

};

const options = {

'method': 'post',

'contentType': 'application/json',

'payload': JSON.stringify(requestBody),

'muteHttpExceptions': true // The critical ingredient!

};

const response = UrlFetchApp.fetch(API_URL, options);

// ... Error checking and success parsing will go here ...

} catch (e) {

// ... Error handling will go here ...

}

}

This structure sets the stage. We’ve made the request and prevented Apps Script from crashing on a non-200 response. Now, we need to handle the outcome.

Catching and Parsing Specific HTTP and API Error Responses

With muteHttpExceptions: true, our try block will always complete the UrlFetchApp.fetch() line, but the response object it returns could represent either a success or a failure. The catch (e) block, in this setup, is reserved for unexpected execution errors—things like a typo in UrlFetchApp, network timeouts, or other platform-level issues.

Our primary job inside the try block is to inspect the HTTP response code. If it’s not 200 (OK), we must treat it as an error. The best practice is to manually throw a custom, more informative error object that our catch block can then handle in a standardized way.

This creates a clean separation:

  1. try block: Makes the call, checks for HTTP success (200), and if it fails, packages up the details and throws them to the catch block.

  2. **catch block: Acts as a single, unified location for processing all failures, whether they are HTTP errors from the API or unexpected JavaScript exceptions.

Let’s see how to parse the response. When the Gemini API fails, it doesn’t just send an error code; it sends a JSON payload explaining what went wrong. This is debugging gold.


// Inside the try block, after the UrlFetchApp.fetch() call:

const responseCode = response.getResponseCode();

const responseBody = response.getContentText();

if (responseCode === 200) {

// This is the success path!

const result = JSON.parse(responseBody);

// ... process and log success ...

return result;

} else {

// This is an API or HTTP error. Package and throw.

const errorDetails = {

message: `Gemini API call failed with status code ${responseCode}`,

statusCode: responseCode,

responseBody: responseBody,

requestPayload: requestBody // Include the original request for debugging

};

// Throwing this sends control to the catch block

throw new Error(JSON.stringify(errorDetails));

}

Now, the catch block receives a well-structured error, whether it came from our throw or from a runtime exception. It can parse this and log everything needed for a post-mortem.

Logging Success and Failure Events with Rich Context

Logging is the entire point of building this robust system. A log entry that just says “Failed” is useless. A log entry that tells you the exact request, the API’s exact error response, the timestamp, and the model used is a lifesaver.

On Success, Log:

  • Confirmation: A simple message like “Gemini API Call Successful”.

  • Model: The model used (gemini-pro).

  • **Usage Metadata: The Gemini API response includes usageMetadata with promptTokenCount and candidatesTokenCount. Logging this is essential for monitoring costs and usage patterns.

  • Execution Time: Calculate the time between starting the try block and getting a successful response.

On Failure, Log Everything:

  • Error Message: The top-level message from your custom error or the JavaScript exception.

  • HTTP Status Code: e.g., 429 for rate limiting, 400 for a bad request.

  • Full API Response Body: This contains the specific error message from Google, like [SAFETY], Invalid API key, etc.

  • **Original Request Payload: This is crucial. If a request fails, you need to know what you sent to be able to reproduce and fix the issue.

By logging this rich context, you transform your Apps Script execution logs from a barren wasteland of “Exception: Error” into a detailed, actionable audit trail.

Code Deep Dive: A Complete Example

Let’s assemble all these pieces into a complete, reusable function. This example uses placeholder logSuccess and logError functions, which you would replace with your own logging implementation (e.g., writing to a Google Sheet, a Firestore database, or just Logger.log).


/**

* A robust function to call the Gemini API, with detailed error handling and logging.

*

* @param {string} prompt The text prompt to send to the model.

* @returns {object|null} The parsed JSON response from the API on success, or null on failure.

*/

function callGeminiWithRobustErrorHandling(prompt) {

const API_KEY = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');

if (!API_KEY) {

logError('API Key is not set in Script Properties.');

return null;

}

const API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`;

const requestBody = {

"contents": [{

"parts": [{

"text": prompt

}]

}],

// Optional: Add safety settings and generation config here

// "safetySettings": [...],

// "generationConfig": {...}

};

const options = {

'method': 'post',

'contentType': 'application/json',

'payload': JSON.stringify(requestBody),

'muteHttpExceptions': true // This is key to custom error handling

};

try {

const startTime = new Date();

// Make the API call

const response = UrlFetchApp.fetch(API_URL, options);

const endTime = new Date();

const duration = endTime - startTime; // Execution time in ms

const responseCode = response.getResponseCode();

const responseBody = response.getContentText();

// Check for a successful response (HTTP 200)

if (responseCode === 200) {

const parsedResponse = JSON.parse(responseBody);

logSuccess({

message: 'Gemini API call successful.',

model: 'gemini-pro',

durationMs: duration,

usage: parsedResponse.usageMetadata || 'Not available'

});

// Extract and return the core content

return parsedResponse.candidates[0].content.parts[0].text;

} else {

// If it's not a 200, it's an error. Throw to the catch block.

throw {

name: 'GeminiApiError',

message: `API returned non-200 status.`,

statusCode: responseCode,

responseBody: responseBody,

requestPayload: requestBody

};

}

} catch (e) {

// This block catches both thrown errors and runtime exceptions

logError({

message: e.message || 'An unexpected error occurred.',

errorName: e.name,

statusCode: e.statusCode || 'N/A',

apiResponseBody: e.responseBody || 'N/A',

requestPayload: e.requestPayload || requestBody, // Log what we tried to send

stack: e.stack || 'N/A'

});

return null; // Return a predictable null on failure

}

}

// --- Placeholder Logging Functions ---

function logSuccess(details) {

console.log(`✅ SUCCESS: ${details.message}`, JSON.stringify(details, null, 2));

// In a real app, you'd write this to a Google Sheet, Logsnag, etc.

}

function logError(errorDetails) {

console.error(`❌ ERROR: ${errorDetails.message}`, JSON.stringify(errorDetails, null, 2));

// In a real app, you'd write this to a dedicated error log and maybe send a notification.

}

This function is now a resilient building block. It centralizes the complex logic of API interaction, provides rich debugging information when things go wrong, and returns a predictable output, making the rest of your agent’s code cleaner and more reliable.

Advanced Techniques for Enterprise-Grade Reliability

Once you’ve established a solid foundation for logging, the next step is to evolve your script from being merely “debuggable” to being truly “resilient.” In an enterprise context, especially when your Apps Script agent is the critical link between users and an AI service, you can’t afford to have it fail silently or brittlely. This section covers three powerful techniques that will dramatically increase your application’s robustness, minimize manual intervention, and provide proactive insights when things go wrong.

Implementing Automated Retries with Exponential Backoff

When your script communicates with external services—like the Gemini or OpenAI APIs—you’re entering a world of transient errors. The network might hiccup, the API might be temporarily overloaded (503 Service Unavailable), or you might hit a rate limit (429 Too Many Requests). A naive script would fail immediately. A robust script tries again, intelligently.

Exponential backoff is a standard error-handling strategy where you progressively increase the waiting time between retries. Instead of hammering a struggling service, you give it space to recover. Adding a small amount of randomness (“jitter”) to the delay prevents multiple instances of your script from retrying in perfect sync, which can cause a “thundering herd” problem.

Here’s a generic wrapper function you can use to add this capability to any function that might fail due to network issues.


/**

* Executes a function with an exponential backoff retry mechanism.

*

* @param {function} func The function to execute.

* @param {number} maxRetries The maximum number of retries. Default is 5.

* @param {number} initialDelayMs The initial delay in milliseconds. Default is 1000.

* @return {*} The return value of the successful function execution.

* @throws {Error} Throws the last captured error if all retries fail.

*/

function withRetry(func, maxRetries = 5, initialDelayMs = 1000) {

let lastError;

let delay = initialDelayMs;

for (let i = 0; i < maxRetries; i++) {

try {

// Attempt to execute the function

return func();

} catch (e) {

lastError = e;

log({

message: `Attempt ${i + 1} of ${maxRetries} failed. Retrying in ${delay}ms...`,

level: "WARN",

error: e,

context: { functionName: func.name || 'anonymous' }

});

// If this was the last attempt, break the loop

if (i === maxRetries - 1) {

break;

}

// Wait for the calculated delay

Utilities.sleep(delay);

// Increase the delay for the next attempt (exponential backoff)

// Add jitter (randomness) to prevent synchronized retries

delay = delay * 2 + Math.floor(Math.random() * 1000);

}

}

log({

message: `All ${maxRetries} retry attempts failed.`,

level: "CRITICAL",

error: lastError,

context: { functionName: func.name || 'anonymous' }

});

throw new Error(`Function failed after ${maxRetries} attempts: ${lastError.message}`);

}

// --- Example Usage ---

/**

* A mock function that simulates a call to a flaky API.

*/

function callFlakyAiApi() {

console.log("Attempting to call the AI API...");

// Simulate a 50% failure rate

if (Math.random() > 0.5) {

throw new Error("API Service Unavailable (503)");

}

console.log("API call successful!");

return { success: true, data: "AI-generated content" };

}

/**

* Main function demonstrating the retry wrapper.

*/

function processDataWithAi() {

try {

const result = withRetry(callFlakyAiApi);

console.log("Successfully processed data:", result);

// ... do something with the result

} catch (e) {

console.error("Could not process data even after retries:", e.message);

// Handle the final failure

}

}

By wrapping your potentially flaky API calls with withRetry(), you build a self-healing mechanism directly into your code, dramatically improving the user experience and the reliability of your automation.

Using Custom Error Objects for Granular Debugging

A standard JavaScript Error object gives you a message and a stack trace. That’s a good start, but for complex applications, it’s often not enough. When an API call fails, you need to know more:

  • What was the HTTP status code?

  • What was the exact request payload we sent?

  • Which specific part of the workflow was running?

This is where custom error objects shine. By extending the base Error class, you can create your own error types that carry this rich, structured context with them. When you log these custom errors, your Monitor Sheet instantly becomes a goldmine for debugging.

Let’s define a specific ApiError for our AI agent.


/**

* A custom error class for handling API-specific failures.

* Extends the built-in Error class to include structured context.

*/

class ApiError extends Error {

/**

* @param {string} message A human-readable error message.

* @param {object} details A collection of contextual details.

* @param {number} [details.statusCode] The HTTP status code of the response.

* @param {string} [details.endpoint] The API endpoint that was called.

* @param {object} [details.requestPayload] The payload sent in the request.

*/

constructor(message, details = {}) {

// Pass the message to the parent Error constructor

super(message);

// Maintains proper stack trace for where our error was thrown (important!)

if (Error.captureStackTrace) {

Error.captureStackTrace(this, ApiError);

}

// A unique name for this error type

this.name = 'ApiError';

// Assign custom properties

this.statusCode = details.statusCode || null;

this.endpoint = details.endpoint || 'unknown';

this.requestPayload = details.requestPayload || {};

}

}

// --- Example Usage ---

function callGeminiApi(prompt) {

const endpoint = 'https://generativelanguage.googleapis.com/...';

const payload = {

// ... your request body

};

const options = {

method: 'post',

contentType: 'application/json',

payload: JSON.stringify(payload),

headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },

muteHttpExceptions: true // CRITICAL! This prevents UrlFetchApp from throwing a generic error

};

const response = UrlFetchApp.fetch(endpoint, options);

const responseCode = response.getResponseCode();

const responseBody = response.getContentText();

if (responseCode !== 200) {

// Instead of a generic error, throw our custom, context-rich error

throw new ApiError(`Gemini API request failed with status ${responseCode}`, {

statusCode: responseCode,

endpoint: endpoint,

requestPayload: payload

});

}

return JSON.parse(responseBody);

}

function mainFunction() {

try {

const result = callGeminiApi("Summarize this document...");

} catch (e) {

// Now we can handle different error types specifically

if (e instanceof ApiError) {

log({

message: `An API error occurred: ${e.message}`,

level: "CRITICAL",

// The error object 'e' itself contains all the rich context!

error: e

});

} else {

// It was a different, unexpected error (e.g., TypeError)

log({

message: "An unexpected non-API error occurred.",

level: "ERROR",

error: e

});

}

}

}

When you log the ApiError instance, your JSON-stringified error column in the spreadsheet will contain the statusCode, endpoint, and requestPayload, giving you instant, actionable insight without having to dig through code or guess what happened.

Setting Up Automated Alerts Based on Your Monitor Sheet

A log is only useful if someone looks at it. For critical failures, you need the system to be proactive. By using a time-driven trigger in Apps Script, you can create a monitor that periodically scans your log sheet and alerts you via Email or Google Chat when certain conditions are met.

Here’s a function you can schedule to run every hour.


// This should be stored as a Script Property for easy management

const SCRIPT_PROPERTIES = PropertiesService.getScriptProperties();

const CHAT_WEBHOOK_URL = SCRIPT_PROPERTIES.getProperty('CHAT_WEBHOOK_URL');

const ADMIN_EMAIL = '[email protected]';

/**

* Scans the error log sheet for recent critical errors and sends an alert.

* This function should be run on a time-based trigger (e.g., every hour).

*/

function checkLogsAndAlert() {

const ss = SpreadsheetApp.openById('YOUR_SPREADSHEET_ID');

const sheet = ss.getSheetByName('ErrorLog');

if (!sheet) return;

const data = sheet.getDataRange().getValues();

const headers = data.shift(); // Get header row

const now = new Date();

const oneHourAgo = new Date(now.getTime() - (60  *60*  1000));

// Find the index of relevant columns

const timestampIdx = headers.indexOf('timestamp');

const levelIdx = headers.indexOf('level');

const messageIdx = headers.indexOf('message');

const recentCriticalErrors = data.filter(row => {

const timestamp = new Date(row[timestampIdx]);

const level = row[levelIdx];

return level === 'CRITICAL' && timestamp > oneHourAgo;

});

if (recentCriticalErrors.length > 0) {

sendAlert(recentCriticalErrors, headers);

}

}

/**

* Composes and sends an alert to Google Chat and/or Email.

* @param {Array<Array<string>>} errors The rows of critical errors.

* @param {Array<string>} headers The header row of the sheet.

*/

function sendAlert(errors, headers) {

const logSheetUrl = `https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/`;

let summary = `Found ${errors.length} critical error(s) in the last hour.\n\n`;

errors.forEach((errorRow, index) => {

const message = errorRow[headers.indexOf('message')];

const timestamp = new Date(errorRow[headers.indexOf('timestamp')]).toLocaleString();

summary += `${index + 1}. [${timestamp}] ${message}\n`;

});

summary += `\n<${logSheetUrl}|View Full Log Sheet>`;

// Option 1: Send to Google Chat (Recommended for teams)

if (CHAT_WEBHOOK_URL) {

const payload = {

'cardsV2': [{

'cardId': 'criticalErrorCard',

'card': {

'header': {

'title': '🚨 Critical AI Agent Error Alert',

'subtitle': 'Automated Monitoring System',

'imageUrl': 'https://developers.google.com/chat/images/errors-api.png',

'imageType': 'CIRCLE'

},

'sections': [{

'widgets': [{

'textParagraph': {

'text': summary

}

}]

}]

}

}]

};

const options = {

method: 'post',

contentType: 'application/json',

payload: JSON.stringify(payload)

};

UrlFetchApp.fetch(CHAT_WEBHOOK_URL, options);

}

// Option 2: Send an Email

const emailSubject = `[Alert] Critical Errors Detected in AI Agent Script`;

MailApp.sendEmail(ADMIN_EMAIL, emailSubject, summary.replace(/<.*\|(.*)>/g, '$1')); // Simple text version for email

}

To make this work:

  1. Set up a Trigger: In the Apps Script Editor, go to Triggers (the clock icon), click + Add Trigger, choose checkLogsAndAlert, select Time-driven as the event source, and set it to run on an Hour timer.

  2. Enable Webhook (for Chat): Go to a Google Chat space, click the space name > Apps & integrations > Manage webhooks, create a new webhook, and copy the URL.

  3. Store Secrets: Store the webhook URL in Script Properties (File > Project properties > Script properties) rather than hardcoding it. This is more secure and easier to manage.

Conclusion: From Fragile Script to Resilient Architecture

We’ve journeyed far beyond the simple Logger.log() and the monolithic try...catch block. The initial excitement of making an AI agent work within Genesis Engine AI Powered Content to Video Production Pipeline can quickly fade when that script fails silently in the middle of the night, leaving you with corrupted data and a frustrated team. The transition from a functional-but-fragile script to a resilient, production-grade architecture isn’t just an upgrade—it’s a fundamental shift in mindset. It’s the difference between a clever hack and a reliable business asset. By embracing robust logging and intelligent error handling, you build systems that you can trust, that can self-diagnose, and that provide the deep insights needed to operate and improve complex AI-driven workflows.

Recap: The Pillars of Production-Ready Automation

The architecture we’ve built rests on a few core pillars. Moving forward, treat these not as optional extras, but as the foundational requirements for any mission-critical automation you deploy.

  • Structured, Centralized Logging: We’ve moved from ephemeral Logger.log messages to a persistent, queryable logging system within a Google Sheet. By logging JSON objects with consistent fields like executionId, severity, functionName, and contextual payloads (like the AI prompt and its raw response), we’ve created a rich dataset for debugging, performance analysis, and auditing AI behavior over time.

  • Granular and Intentional Error Handling: A single try...catch is a blunt instrument. We’ve implemented a more surgical approach. This includes using custom error classes to differentiate between transient network issues, fatal API errors, and data validation failures. This granularity allows our system to react intelligently—retrying when appropriate, alerting a human when necessary, and failing gracefully without corrupting state.

  • Proactive Monitoring and Alerting: A resilient system doesn’t wait for a user to report a problem. By implementing automated email alerts triggered by specific error thresholds or critical failure types, we’ve built a system that actively notifies us when things go wrong. This proactive stance is crucial for maintaining service level objectives and building trust with stakeholders.

  • Configuration-Driven Design: Hardcoding API keys, sheet IDs, and alert thresholds directly into the code is a recipe for disaster. By abstracting these values into Script Properties, we’ve decoupled the logic from its environment. This makes our agent safer, easier to maintain, and seamlessly portable between development, staging, and production environments.

Take the Next Step to Scale Your Business Operations

Implementing these patterns is more than just a technical exercise; it’s a strategic investment in scalability. Every hour you don’t have to spend manually debugging a failed script is an hour you can spend developing new features or improving your AI models. A system that logs its own actions and intelligently handles its own failures is a system that can run and scale with minimal human intervention.

Your journey doesn’t end here. Use this framework as a launchpad:

  1. Refactor a Critical Script: Identify an existing Apps Script project that is vital to your operations but lacks robust error handling. Apply the principles from this guide to fortify it. Experience firsthand the confidence that comes from knowing it won’t fail silently.

  2. Build a Reusable Library: Abstract your logging and error-handling functions into a standalone library that you can easily import into all future Apps Script projects. Create a template to standardize your approach and accelerate development.

  3. Explore Advanced Integrations: For enterprise-level scale, consider pushing your logs beyond Google Sheets. Explore integrating with Google Cloud Logging for more powerful querying, visualization, and alerting capabilities. Investigate using a tool like clasp to bring modern development practices like version control (Git) and local development to your Apps Script workflow.

You now have the blueprint to build automations that don’t just work, but endure. You can create AI agents that are not only powerful but also predictable, manageable, and ready to become a core, reliable part of your business’s operational engine.


Tags

Google Apps ScriptAI AgentsError HandlingLoggingGoogle WorkspaceAutomationGemini API

Share


Previous Article
Scale Your Sales Automate Personalized Proposals with Gemini and Google Workspace
Vo Tu Duc

Vo Tu Duc

A Google Developer Expert, Google Cloud Innovator

Stop Doing Manual Work. Scale with AI.

Hi, I'm Vo Tu Duc (Danny), a recognised Google Developer Expert (GDE). I architect custom AI agents and Google Workspace solutions that help businesses eliminate chaos and save thousands of hours.

Want to turn these blog concepts into production-ready reality for your team?
Book a Discovery Call

Table Of Contents

1
The High Stakes of AI [Automated Job Creation in Real Time Jobber and Google Sheets Integration from Gmail](https://votuduc.com/Automated-Job-Creation-in-Jobber-from-Gmail-p115606) in [Automatically create new folders in Google Drive, generate templates in new folders, fill out text automatically in new files, and save info in [Automated Web Scraping with [Multilingual Text-to-Speech Tool with SocialSheet Streamline Your Social Media Posting 123](https://votuduc.com/Multilingual-Text-to-Speech-Tool-with-Google-Workspace-p809282)](https://votuduc.com/Automated-Web-Scraping-with-Google-Sheets-p292968)](https://workspace.google.com/marketplace/app/auto_create_folder_and_files/430076014869)
2
Architecting the Automation Monitor: A Pragmatic Approach
3
The Bulletproof Try-Catch Pattern for Gemini API Calls
4
Advanced Techniques for Enterprise-Grade Reliability
5
Conclusion: From Fragile Script to Resilient Architecture

Portfolios

AI Agentic Workflows
Cloud Engineering
AppSheet Solutions
Change Management
Strategy Playbooks
Product Showcase
Uncategorized
Workspace Automation

Related Posts

Automate Site Defect Punch Lists with Gemini and Google Chat
May 22, 2026
© 2026, All Rights Reserved.
Powered By

Quick Links

Book a CallAbout MeVolunteer Legacy

Social Media