Why Google Apps Script Couldn’t Convert My H2 Headings (and the Fix That Actually Works)
If you’ve ever tried to programmatically convert Heading 2 → Heading 3 in Google Docs using Apps Script, you probably expected something simple like:
paragraph.setHeading(DocumentApp.ParagraphHeading.HEADING_3);
And yet… nothing happened.
Or worse:
Exception: Unexpected error while getting the method or property setHeading on object DocumentApp.Paragraph
That’s exactly what happened in this document — even though the Google Docs UI clearly showed the text as Heading 2.
This post explains why that happens, why most scripts fail, and the reliable solution that finally works.
The Problem: “They’re H2… but Apps Script says they aren’t”
Visually, headings like:
13.13 Common Mindset Traps (and How to Avoid Them)
were clearly marked as Heading 2 in the Google Docs styles menu.
But every standard approach failed:
getHeading() === HEADING_2→ returned nothing- Font-based detection (Arimo, 18pt) → returned
0 - Recursive document walkers → still missed them
- Even rebuilding paragraphs sometimes threw errors
The giveaway error
Exception: Unexpected error while getting the method or property setHeading
This error is a huge red flag that the paragraph object itself is malformed.
What Was Really Happening (the root cause)
This document had gone through copy/paste and content migration (very common with:
- Word → Google Docs
- AI-generated content
- Web → Docs pastes
- Long-form books stitched together
Internally, Google Docs can end up with:
- Paragraphs that look like Heading 2
- UI style dropdown says “Heading 2”
- BUT the underlying paragraph object is not mutable by
DocumentApp
In short:
The visual style and the scriptable structure were out of sync
This is a known (but undocumented) limitation of DocumentApp.
Why common solutions fail
❌ getHeading() checks
These rely on the DocumentApp paragraph model, which can lie or break after pasting.
❌ Font & size detection
Headings often inherit font and size from styles, so Apps Script returns null.
❌ Clearing formatting
Character formatting ≠ paragraph style metadata.
❌ Rebuilding paragraphs (sometimes)
Even rebuilding can fail when the parent container is corrupted.
The Fix That Actually Works: Use the Google Docs API
Instead of DocumentApp, we switch to the Google Docs API, which operates on the actual document structure — not the fragile Apps Script abstraction.
This lets us update the paragraph’s namedStyleType directly:
HEADING_2 → HEADING_3
Even when setHeading() explodes.
Step 1: Enable the Google Docs API
In Apps Script:
- Open Extensions → Apps Script
- Click Services (puzzle icon)
- Add Google Docs API
- Save
Step 2: Working Solution — Convert H2 → H3 Reliably
/**
* Converts all Heading 2 paragraphs to Heading 3
* using the Google Docs API.
*
* This works even when DocumentApp.setHeading() fails
* due to pasted or corrupted paragraph structures.
*/
function convertH2toH3_withDocsApi() {
const docId = DocumentApp.getActiveDocument().getId();
const doc = Docs.Documents.get(docId);
const requests = [];
const content = doc.body.content || [];
for (const el of content) {
const p = el.paragraph;
if (!p || !p.paragraphStyle) continue;
if (p.paragraphStyle.namedStyleType === "HEADING_2") {
const startIndex = el.startIndex;
const endIndex = el.endIndex;
if (typeof startIndex === "number" && typeof endIndex === "number") {
requests.push({
updateParagraphStyle: {
range: { startIndex, endIndex },
paragraphStyle: { namedStyleType: "HEADING_3" },
fields: "namedStyleType"
}
});
}
}
}
if (requests.length === 0) {
Logger.log("No HEADING_2 paragraphs found.");
return;
}
// Batch updates to avoid API limits
const CHUNK_SIZE = 500;
for (let i = 0; i < requests.length; i += CHUNK_SIZE) {
Docs.Documents.batchUpdate(
{ requests: requests.slice(i, i + CHUNK_SIZE) },
docId
);
}
Logger.log(`Converted ${requests.length} Heading 2 items to Heading 3.`);
}
Why this works (when nothing else does)
| Method | Reliability |
|---|---|
| DocumentApp.setHeading | ❌ Breaks on pasted docs |
| Font detection | ❌ Inherited styles return null |
| Paragraph rebuilding | ⚠️ Inconsistent |
| Docs API | ✅ Reliable |
The Docs API:
- Operates on namedStyleType
- Ignores broken paragraph wrappers
- Works even when the UI and Apps Script disagree
Resizing the headings (best practice)
After conversion, don’t script font sizes unless you must.
Instead:
- Click one Heading 3
- Set the font/size you want (e.g., Arimo 16)
- Format → Paragraph styles → Heading 3 → Update “Heading 3” to match
This instantly normalizes the entire document and prevents future issues.
When should you use the Docs API?
Use it when:
- You see “Unexpected error” from
DocumentApp - Headings visually exist but scripts can’t find them
- You’re processing long or migrated documents
- You’re building book-scale automations
Final takeaway
If Google Docs styles were a clean, reliable system, this wouldn’t be necessary — but in real-world documents, they aren’t.
When Apps Script fails silently or throws cryptic errors:
Drop down one level and use the Google Docs API.
It’s the difference between fighting formatting ghosts… and fixing the problem in one run.