Time-driven triggers and overlapping edits collide. withScriptLock waits then throws if it can't acquire — great when failure must be loud. tryLockOrSkip returns null instead, so a second run can quietly exit. Use it when "already running" is a normal skip, not an error.
What you'll need
- A container-bound or standalone Apps Script project
- Overlapping executions (time-driven triggers, multi-user edits, or Installable + simple triggers)
- A clear place to early-return when the lock is busy
How to use this snippet
/**
* Try a script or document lock. Returns the lock, or null if busy.
* Distinct from withScriptLock, which throws when it can't acquire.
* @param {'script'|'document'} kind
* @param {number=} timeoutMs wait budget for tryLock (default 0 → ~1ms probe)
* @return {GoogleAppsScript.Lock.Lock|null}
*/
function tryLockOrSkip(kind, timeoutMs) {
timeoutMs = timeoutMs == null ? 0 : timeoutMs;
var lock = (kind === 'document')
? LockService.getDocumentLock()
: LockService.getScriptLock();
var ok = timeoutMs > 0 ? lock.tryLock(timeoutMs) : lock.tryLock(1);
if (!ok) return null;
return lock;
}
Example
function nightlySync() {
var lock = tryLockOrSkip('script', 0);
if (!lock) {
console.log('nightlySync: skip — another run holds the lock');
return;
}
try {
// … exclusive work …
} finally {
lock.releaseLock();
}
}
Tips:
- Prefer
scriptfor project-wide jobs;documentwhen only this spreadsheet's concurrent editors matter. - Always
releaseLock()infinally— a skipped acquire never needs release. - Pair with a Log sheet line when you skip, so "did it run?" is answerable.
- If skipping is wrong (money movement, once-only claims), use
withScriptLockand let it throw.
Tip: NitroGAS Co-Pilot can paste this next to your trigger entrypoint and wire the early-return — soft assist, you keep the judgment call.
Happy Coding!
