Retry
If you want to retry a task that had failed a couple of times more, you can use the retry property in the Task.
Example
You can find the related examples here.
Usage
import { delay, Listr } from 'listr2'
interface Ctx {
output: string
}
const tasks = new Listr<Ctx>(
[
{
title: 'Some type errors',
task: async(_, task): Promise<void> => {
await delay(1000)
task.output = 'test'
await delay(1000)
throw new Error('This type can not be assigned to type with, oh noes')
},
retry: 3
}
],
{
concurrent: false,
exitOnError: true
}
)
const context = await tasks.run()Retry Delay
v6.0.0 #668Retry action can have a delay between the tries. For enabling this behavior, you can pass the retry to the given task as an object.
import { delay, Listr } from 'listr2'
interface Ctx {
output: string
}
const tasks = new Listr<Ctx>(
[
{
title: 'Some type errors',
task: async(_, task): Promise<void> => {
await delay(1000)
task.output = 'test'
await delay(1000)
throw new Error('This type can not be assigned to type with, oh noes')
},
retry: {
delay: 1000,
tries: 3
}
}
],
{
concurrent: false,
exitOnError: true
}
)
const context = await tasks.run()While waiting between attempts the task enters a paused state, and the DefaultRenderer shows a live countdown until the next try through its pausedTimer option. You can check for this state inside a task with task.isPaused().
Dynamic Retry Delay
#791The delay can also be a callback that returns the time to wait in milliseconds, which makes it possible to implement backoff strategies like an exponential backoff. The callback will be evaluated before every retry with the current retry attempt, where the count is the number of the failed attempt and error is the error it has thrown, and the context. It can also return a Promise.
import { delay, Listr } from 'listr2'
const tasks = new Listr(
[
{
title: 'Some type errors',
task: async(): Promise<void> => {
await delay(1000)
throw new Error('This type can not be assigned to type with, oh noes')
},
retry: {
tries: 3,
delay: ({ count }): number => Math.min(1000 * 2 ** (count - 1), 30000)
}
}
],
{
concurrent: false,
exitOnError: true
}
)
await tasks.run()- If the callback throws, the task fails with that error without retrying further.
- If the run is interrupted while the callback is pending, the task does not wait for it and stops with the error of the failed attempt.
Fail Without Retrying
#791Whenever the task knows that retrying will never succeed, it can fail immediately by returning task.fail(error). The task fails with the given error through the regular error handling, which means rollback, exitOnError and error collection behave the same as an error that has been thrown in the last attempt, but the remaining retries are not used.
import type { ListrTaskFailure } from 'listr2'
import { delay, Listr } from 'listr2'
const tasks = new Listr(
[
{
title: 'Clean storage',
task: async(_, task): Promise<ListrTaskFailure> => {
await delay(1000)
if (task.isRetrying().count > 0) {
return task.fail(new Error('Storage is locked by another process.'))
}
throw new Error('Storage is not ready yet.')
},
retry: {
tries: 10,
delay: 1000
}
},
{
title: 'This task will still run',
task: async(): Promise<void> => {
await delay(3000)
}
}
],
{
concurrent: true,
exitOnError: false
}
)
await tasks.run()- The value of
task.fail()has to be returned from the task, calling it without returning it will not fail the task. Throwing it, i.e.throw task.fail(error), behaves the same as returning it. - The given error is passed through as it is, without any validation or wrapping.
- It only applies to the task that calls it. When a subtask fails itself and its error fails the parent task, the parent task still uses its own retries as usual.
- Unlike
task.skip(), the task ends up in the failed state, and unliketask.cancel(), it does not interrupt the other tasks.
Retry Event
Retrying is self-aware, and you can access the task if it is retrying via task.isRetrying(). It will either return an object with the given interface where the count will be 0 for not repeating tasks, and error is the last encountered error if retrying.
Retry Count
import { delay, Listr } from 'listr2'
const tasks = new Listr(
[
{
title: 'Some thing with errors',
task: async(_, task): Promise<void> => {
const retry = task.isRetrying()
if (retry.count > 0) {
task.title = 'This means I am retrying.'
task.output = ['I am self aware that I am retrying for the %dth time.', retry.count]
}
await delay(1000)
throw new Error('This type can not be assigned to type with, oh noes')
},
retry: 3
}
],
{ exitOnError: false }
)
await tasks.run()Last Error
import { delay, Listr } from 'listr2'
const tasks = new Listr(
[
{
title: 'Some thing with errors',
task: async(_, task): Promise<void> => {
const retry = task.isRetrying()
if (retry.count > 0) {
// eslint-disable-next-line no-constant-binary-expression
if (retry.error === new Error('Something')) {
task.title = 'I will process the task further.'
}
}
await delay(1000)
throw new Error('This type can not be assigned to type with, oh noes')
},
retry: 3
}
],
{ exitOnError: false }
)
await tasks.run()Renderer
- When retrying, the task title will be reset to the original task title.
DefaultRenderer
Details
suffixRetries?
optionalsuffixRetries?:boolean
Defined in: packages/listr2/src/renderer/default/renderer.interface.ts:139
Suffix retry messages to clearly indicate the task is currently retrying.
truewill add[RETRY:COUNT]as a suffix.falsewill not add a suffix.
Default Value
false
pausedTimer?
optionalpausedTimer?:PresetTimer
Defined in: packages/listr2/src/renderer/default/renderer.interface.ts:148
Show duration for the pauses.
Default Value
SimpleRenderer & VerboseRenderer
The non-TTY renderers log a RETRY entry with the current attempt count on each retry, rather than resetting an in-place title.