Error Handling ​
Exceptions that occur while running the Task will be handled internally through Listr. You can throw errors out of the tasks to show they are unsuccessful or stop execution. This can further be customized at Listr, Subtask or Task level.
Errors will yield a visual output on the terminal depending on the current renderer, and will also handle the Task that has failed depending on the configuration. If an application needs to quit prematurely and fail a specific task just throw out an instance of Error.
The default behavior is if any of the tasks have failed, it will deem itself as unsuccessful and exit. This behavior can be changed with the exitOnError option. If the exitOnError is true, the first error encountered will be thrown out, and it will propagate outwards starting from the Task.
WARNING
An Error should be always a real Error type extended from the JavaScript/Typescript Error class.
Example
You can find the related examples here.
Throwing a Error ​
Throwing an error will stop any further action from the current Task and will propagate outwards of the Task to Listr and depending on the exitOnError configuration, execution will be slowly halted for upcoming or concurrent tasks.
INFO
You don't have to catch errors explicitly since they will always be handled and surfaced by Listr. Collecting them into Listr.errors for later inspection is a separate, opt-in behavior covered below.
WARNING
Be aware that the execution will only stop after the error is thrown out. This can kill any asynchronous action prematurely.
import { delay, Listr } from 'listr2'
const tasks = new Listr(
[
{
title: 'This task will fail.',
task: async(): Promise<void> => {
await delay(2000)
throw new Error('This task failed after 2 seconds.')
}
},
{
title: 'This task will never execute.',
task: (ctx, task): void => {
task.title = 'I will change my title if this executes.'
}
}
],
{ concurrent: false }
)
await tasks.run()Changing the Behavior ​
Per Listr ​
import { delay, Listr } from 'listr2'
const tasks = new Listr(
[
{
title: 'This task will fail.',
task: async(): Promise<void> => {
await delay(2000)
throw new Error('This task failed after 2 seconds.')
}
},
{
title: 'This task will execute.',
task: (ctx, task): void => {
task.title = 'I will change my title if this executes.'
}
}
],
{ concurrent: false, exitOnError: false }
)
await tasks.run()Per Subtask ​
import { delay, Listr } from 'listr2'
const tasks = new Listr(
[
{
title: 'This task will execute and not quit on errors.',
task: (ctx, task): Listr =>
task.newListr(
[
{
title: 'This is a subtask.',
task: async(): Promise<void> => {
throw new Error('I have failed [0]')
}
},
{
title: 'This is yet an another subtask and it will run.',
task: async(ctx, task): Promise<void> => {
task.title = 'I have succeeded.'
}
}
],
{ exitOnError: false }
)
},
{
title: 'This task will execute.',
task: (): void => {
throw new Error('I will exit on error since I am a direct child of parent task.')
}
}
],
{ concurrent: false, exitOnError: true }
)
await tasks.run()Per Task ​
import { delay, Listr } from 'listr2'
const tasks = new Listr(
[
{
title: 'This task will fail.',
task: async(): Promise<void> => {
await delay(2000)
throw new Error('This task failed after 2 seconds.')
},
exitOnError: false
},
{
title: 'This task will execute.',
task: (_, task): void => {
task.title = 'I will change my title if this executes.'
}
}
],
{
concurrent: false,
exitOnError: true
}
)
await tasks.run()Renderer ​
DefaultRenderer ​
Default renderer has options where you can change how the errors are displayed.
Interface
showErrorMessage? ​
optionalshowErrorMessage?:boolean
Defined in: packages/listr2/src/renderer/default/renderer.interface.ts:118
Show the error message or show the original title of the task.
truewill output the current error encountered with the task if there is any.falsewill keep the current task title intact. This will also disablecollapseErrors.
Default Value ​
true
collapseErrors? ​
optionalcollapseErrors?:boolean
Defined in: packages/listr2/src/renderer/default/renderer.interface.ts:127
Collapse error messages into a single message and overwrite the task title.
truewill collapse the error message.falsewill show the error message as a data output under the current task title.
Default Value ​
true
SimpleRenderer & VerboseRenderer ​
The non-TTY renderers log a FAILED entry carrying the error message.
Collected Errors ​
Errors from the Task are collected inside an array in the main Listr task list as tasks.errors where tasks is the Listr class. This option is opt-in since v6.0.0.
Since there are options to ignore some errors on cases like exitOnError, or the ability to retry the given task through task.retry, encountered errors can be swallowed while the execution. To deal with those swallowed errors, all the errors that are encountered even though it does not stops the execution gets collected through this property.
Enabling ​
#615Error collection is toggled per Task through the Listr options with the key collectErrors, which is a boolean. The default is false since this is the most-underused functionality, and it should be at least opt-in for saving some memory.
Setting it to true will collect where the error has occurred, when it has been encountered and what the error.message is. The context is no longer cloned in to the ListrError to avoid potential memory leaks and issues with cloning non-serializable values.
While collection is disabled, Listr.errors is null instead of an empty array. This way an empty array always means that collection is enabled and no errors have been encountered yet, while null means the errors were never collected. Guard reads with listr.errors?.length or an explicit null check.
ListrError ​
ListrError class extends the default Error and has some additional information like the cause of the error and where it is coming from to further debug the issue while execution.
ListrErrorTypes ​
A listr error can be caused by multiple reasons, for a better explanation of why that particular error occurred, a type property on the ListrError exists in the form of enum ListrErrorTypes.
Reporting an Error Manually ​
A Task can attach an error to the collection without throwing or interrupting its own execution through task.report(error, type), where type is one of ListrErrorTypes. This is handy when a task recovers from a failure but you still want it recorded. The error is only stored while collectErrors is enabled.
task.report(new Error('recovered, but worth noting'), ListrErrorTypes.HAS_FAILED)Methodology ​
The order of the array tasks.errors where tasks is the Listr class, represents the order of errors that are encountered.
To keep the error collection mechanism simple and predictable, it might also process the errors coming from the subtasks as well.
For example, the following example will clear some things up about the given mindset.
Code
import { Listr } from 'listr2'
const tasks = new Listr(
[
{
task: (): void => {
throw new Error('1')
}
},
{
task: (_, task): Listr => {
return task.newListr(
[
{
task: (): void => {
throw new Error('3')
}
},
{
task: (): void => {
throw new Error('4')
}
}
],
{ exitOnError: true }
)
}
},
{
task: (): void => {
throw new Error('2')
}
}
],
{ exitOnError: false, collectErrors: true }
)
await tasks.run()
console.log(tasks.errors)Output
[
Error: 1
at Task.taskFn (file:///home/cenk/development/listr2/examples/docs/task/error-handling/collection.ts:7:15)
at Task.run (file:///home/cenk/development/listr2/src/lib/task.ts:298:35) {
error: Error: 1
at Task.taskFn (file:///home/cenk/development/listr2/examples/docs/task/error-handling/collection.ts:7:15)
at Task.run (file:///home/cenk/development/listr2/src/lib/task.ts:298:35),
type: 'HAS_FAILED_WITHOUT_ERROR',
task: Task {
emitter: [EventEmitter],
listr: [Listr],
task: [Object],
options: [Object],
rendererOptions: {},
id: '5.1ffae60d8631f.efd766868b915.866e1eec8dcb1.6c49d60beb37d.a3a892931a5ef.e78a8650db4c1.74a7a66109265.bcc1d65fd77f-9.16ae5bbd949de.84a3445d953d9.b4ff3c4de3d24.6847e6b03009-48.845a4ab1c8fec.0406e1d03a734.306961606f12-3f.d63122b38bc71.2c6be88b853d.aa5a66a00
fa3-2.67a25dc32b275.05069591f2917.99ad61f61433b.62d2b3fcf014.6c3ab12e8638b.a012e95dfadb6.5ac0b32dde1f5.3783719aafae9.f7cf95351bc3.b468fee844f0.c9627515807f4.3c546e82beb7',
state: 'FAILED',
subtasks: undefined,
title: undefined,
initialTitle: undefined,
output: undefined,
retry: undefined,
message: [Object],
rendererTaskOptions: undefined,
prompt: undefined,
parent: undefined,
enabled: true,
taskFn: [Function: task]
},
path: [ undefined ],
ctx: undefined,
name: 'ListrError'
},
Error: 3
at Task.taskFn (file:///home/cenk/development/listr2/examples/docs/task/error-handling/collection.ts:16:23)
at Task.run (file:///home/cenk/development/listr2/src/lib/task.ts:298:35) {
error: Error: 3
at Task.taskFn (file:///home/cenk/development/listr2/examples/docs/task/error-handling/collection.ts:16:23)
at Task.run (file:///home/cenk/development/listr2/src/lib/task.ts:298:35),
type: 'HAS_FAILED',
task: Task {
emitter: [EventEmitter],
listr: [Listr],
task: [Object],
options: [Object],
rendererOptions: {},
id: 'e.b91e31c7a8633.a5c0d756a30ed.8653c1351752e.73b560a042c80.64e107220cbf3.fc2b7bd6f02f5.c9688ca346799.cb51fffc2226-a.6e976cd950398.b054762b266ad.464a3f3110918.e466da24f4d6-45.96ed406986ecb.11850cc6105df.560383a2d91c-36.0c1b392d14abe.9f89babe09822.45b315ac
4bb8-1.ee031a687fb11.86d5245ac9fc3.55e9067a7e183.53d94301c813.9591c72743037.4922af1ed491f.2bdb77eed4ee.ec002d1dfb8e1.7882a17eb2377.049051202a2e0.0c942ce10bc6b.0d33f80a6803',
state: 'FAILED',
subtasks: undefined,
title: undefined,
initialTitle: undefined,
output: undefined,
retry: undefined,
message: [Object],
rendererTaskOptions: undefined,
prompt: undefined,
parent: [Task],
enabled: true,
taskFn: [Function: task]
},
path: [ undefined, undefined ],
ctx: undefined,
name: 'ListrError'
},
Error: 2
at Task.taskFn (file:///home/cenk/development/listr2/examples/docs/task/error-handling/collection.ts:31:15)
at Task.run (file:///home/cenk/development/listr2/src/lib/task.ts:298:35) {
error: Error: 2
at Task.taskFn (file:///home/cenk/development/listr2/examples/docs/task/error-handling/collection.ts:31:15)
at Task.run (file:///home/cenk/development/listr2/src/lib/task.ts:298:35),
type: 'HAS_FAILED_WITHOUT_ERROR',
task: Task {
emitter: [EventEmitter],
listr: [Listr],
task: [Object],
options: [Object],
rendererOptions: {},
id: '8.255d2f2e3fa9c.8870108a62aee.14a04d8597aa4.da8da95f37b79.575d8732ea0b5.cb0d308db7b40.f19fb8db928d.5ed830c8904b-e.b944062c0ab5f.d339986948bdf.5ad93c6148292.01e745100f4c-47.fe4e954aec826.032487f91f53b.df78f4442433-31.591906fa76151.dbe812f35d5a8.17d8b12a5
3c8-d.6919e94a5b5d7.e0f5853c6cc2e.c1020891d643a.1235a6678d71d.f39b92415cafb.0c07b9db54fc0.02522dfb9af63.c2fd91995b050.ef96265e70f54.d2ad4fff8edd0.ed4249a0f67f6.b78b6510ccf9',
state: 'FAILED',
subtasks: undefined,
title: undefined,
initialTitle: undefined,
output: undefined,
retry: undefined,
message: [Object],
rendererTaskOptions: undefined,
prompt: undefined,
parent: undefined,
enabled: true,
taskFn: [Function: task]
},
path: [ undefined ],
ctx: undefined,
name: 'ListrError'
}
]Flow
- First error will be thrown from the first task. Since exitOnError is
falseon that context,ListrErrorwill get collected bytasks.errors, and the value will be{ message: '1', type: ListrErrorTypes.HAS_FAILED_WITHOUT_ERROR }. - Then it will recurse into the second task, which has two subtasks.
- The first task from the subtasks will fail and since the
exitOnErroris set totruein that context, that subtasks will fail and throw. TheListrErrorappended to thetasks.errorswill be{ message: '3', type: ListrErrorTypes.HAS_FAILED } - Since the subtask has crashed, it will not execute the upcoming tasks in the subtasks.
- It will return to the main task list and execute the 3rd task from that list. It will again show the same behavior with the first task, and the
ListrErrorwill be{ message: '2', type: ListrErrorTypes.HAS_FAILED_WITHOUT_ERROR }.