When a time-driven trigger and a manual run can overlap — or two editors kick the same sync — you need a lock. This helper wraps LockService.getDocumentLock() so unlock always happens in finally.
What you'll need
- A container-bound script (Document Lock is for the spreadsheet / doc the script is bound to)
- A critical section that mutates shared state (sheet writes, watermark updates, appends)
For standalone projects that aren't bound to a document, use LockService.getScriptLock() with the same try/finally shape.
Why try/finally
If you tryLock, do work, and only releaseLock on the happy path, a thrown error leaves the lock held until it times out. Other runs then pile up waiting. finally fixes that.
How to use this snippet
function withDocumentLock(fn, waitMs) {
waitMs = waitMs || 30000;
var lock = LockService.getDocumentLock();
if (!lock.tryLock(waitMs)) {
throw new Error('Could not acquire document lock within ' + waitMs + 'ms');
}
try {
return fn();
} finally {
lock.releaseLock();
}
}
Call it with a function (or arrow-free anonymous function for older V8 habits):
function appendRowSafely(values) {
return withDocumentLock(function () {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Log');
sheet.appendRow(values);
return sheet.getLastRow();
});
}
// appendRowSafely([new Date(), 'synced', 42]);
Tips:
- Keep the locked section small — hold the lock only around the read-modify-write, not around slow
UrlFetchAppcalls when you can avoid it. - Fail loud when the lock can't be acquired; silent skips look like "the sync ran."
- Prefer Document Lock when the contention is this spreadsheet; Script Lock when contention is the project across installs.
Happy Coding!
