Understanding Async and Await
Learn how async and await work in TypeScript, including Promises, parallel execution, async iteration, and avoiding unresolved promise errors.
This is the biggest difference between Groovy and TypeScript. In Groovy, most operations are synchronous and complete immediately. In TypeScript, API calls are asynchronous; they return a Promise that resolves at a later time. Understanding async and await is crucial for writing effective TypeScript scripts.
What is asynchronous programming?
In TypeScript, operations like API calls don't block execution while waiting for a response. Instead, they return a Promise, which is a placeholder for a value that will be available later. You must use the await keyword to retrieve the actual result. For example:
TypeScript:
// ✅ Use await to get the actual data
const issue = await Issues.getByKey('JRA-1')
console.log(issue.key) // Works! issue is now the actual Issue object
// ❌ This doesn't work - returns a Promise, not the actual data
const issue = Issues.getByKey('JRA-1')
console.log(issue.key) // Error! issue is a Promise, not an Issue objectGroovy equivalent:
// In Groovy, this just works synchronously
def issue = Issues.getByKey('JRA-1')
logger.info(issue.key) // No await neededThe async and await keywords
To use the await keyword, your function must be marked as async. This tells TypeScript that the function contains asynchronous operations. await pauses execution until the Promise resolves. For example:
TypeScript:
// Function must be async to use await
const getData = async () => {
const issue = await Issues.getByKey('JRA-1')
return issue
}
// Your script entry point is always async
export default async function () {
const issue = await Issues.getByKey('JRA-1')
return issue
}How many awaits do I need?
Most API calls in ScriptRunner resolve in a single await. However, some APIs, like Fetch, return their response in two stages: headers first, then the body. These require two await calls:
TypeScript:
// HAPI - one await, resolves fully in one step
const issue = await Issues.getByKey('JRA-1')
// Fetch - two awaits, headers and body arrive separately
const response = await fetch('https://api.example.com/data')
const data = await response.json()See the Make API Calls with Fetch page for more details on this pattern.
Sequential vs parallel execution
One advantage of asynchronous programming is that you can run multiple operations in parallel, which is much faster than running them sequentially. You can use Promise.all() when operations don't depend on each other, as this can speed up your scripts. For example:
TypeScript:
// Sequential - operations happen one after another (slower)
// Total time: 3 seconds (1s + 1s + 1s)
const issue1 = await Issues.getByKey('JRA-1') // Wait 1 second
const issue2 = await Issues.getByKey('JRA-2') // Wait 1 second
const issue3 = await Issues.getByKey('JRA-3') // Wait 1 second
// Parallel - operations happen simultaneously (faster!)
// Total time: 1 second (all at once)
const [issue1, issue2, issue3] = await Promise.all([
Issues.getByKey('JRA-1'),
Issues.getByKey('JRA-2'),
Issues.getByKey('JRA-3')
])Groovy equivalent:
// Groovy is always sequential
def issue1 = Issues.getByKey('JRA-1')
def issue2 = Issues.getByKey('JRA-2')
def issue3 = Issues.getByKey('JRA-3')async iteration
When iterating over search results, you use for await...of instead of Groovy's .each(). This is because the search returns results asynchronously, and the await keyword in the loop name tells TypeScript to wait for each result before continuing.
TypeScript:
// Iterate over search results
for await (const issue of Issues.search('project = JRA', {
fields: ['key', 'summary']
})) {
console.log(issue.key)
// You can use await inside the loop
await Issues.update({
issueIdOrKey: issue.key,
body: { fields: { priority: { name: 'High' } } }
})
}Groovy equivalent:
Issues.search('project = JRA').each { issue ->
logger.info(issue.key)
issue.update {
setPriority('High')
}
}Common async patterns
Here are the most common patterns you'll use when working with async operations in ScriptRunner.
When to use each pattern:
- Sequential: When each operation depends on the previous one, or when you need to respect rate limits
- Parallel: When operations are independent and you want maximum speed
- Early exit: When you're searching for a specific condition and can stop once found
- Error handling: Always wrap risky operations in try/catch
TypeScript:
// Pattern 1: Sequential processing (when order matters)
for await (const issue of Issues.search('project = JRA')) {
await processIssue(issue) // Wait for each to complete before moving to next
}
// Pattern 2: Parallel processing (when order doesn't matter - faster!)
const issues = []
for await (const issue of Issues.search('project = JRA')) {
issues.push(issue)
}
// Process all issues at once
await Promise.all(issues.map(issue => processIssue(issue)))
// Pattern 3: Early exit (stop when condition is met)
for await (const issue of Issues.search('project = JRA')) {
if (someCondition) {
break // Stop iteration immediately
}
}
// Pattern 4: Error handling in async operations
try {
const issue = await Issues.getByKey('JRA-1')
await Issues.update({ issueIdOrKey: issue.key, body: { ... } })
} catch (error) {
console.error('Operation failed:', error)
}Always await your promises
ScriptRunner checks that every promise your script starts has finished before your function returns. Forgetting to await is the most common cause of a script that appears to do nothing: if your function finishes while asynchronous work is still in flight, that work may never complete, and the run fails with an error like:
Your script has 2 unresolved Promise(s), please ensure to 'await' where necessary.This usually means you called an async operation (a HAPI method or fetch) without await, or started several operations, often in a loop, without awaiting them. To fix it:
- Add
awaitbefore each async call. - If a helper function makes async calls, mark it
asyncandawaitit where you call it. - To run independent operations together, collect the promises and
await Promise.all([...]).
TypeScript:
// ❌ Wrong: the comment calls are never awaited, so they may not run
issues.forEach((i) => Issues.addComment(i.key, 'Reviewed'))
// ✅ Right: await all of them before the function returns
await Promise.all(issues.map((i) => Issues.addComment(i.key, 'Reviewed')))