The declarative Jenkins Pipeline allows us to define timeout either at the pipeline level or the specific stage. This feature prevents Jenkins’s job from getting stuck. However, in some cases, we want to accept that one stage may timeout, but we want to keep the remaining stages running.

Using stage timeout

Let’s start with a simple example. In this demo, I use a pipeline with two stages, A and B.

Listing 1. Jenkinsfile
pipeline {
    agent any

    stages {
        stage("A") {
            options {
                timeout(time: 3, unit: "SECONDS")
            }

            steps {
                echo "Started stage A"
                sleep(time: 5, unit: "SECONDS")
            }
        }

        stage("B") {
            steps {
                echo "Started stage B"
            }
        }
    }
}

As you may expect, running this pipeline causes timeout in the A stage, and the remaining stage B does not get triggered.

Using catchError workflow step

In most cases, this is OK. The remaining stage of the pipeline depends on the success of the previous one. But what if we want to treat A stage as optional and keep the pipeline running even if it times out? Let’s see if we can use catchError workflow step to handle the exception for us.

Listing 2. Jenkinsfile
pipeline {
    agent any

    stages {
        stage("A") {
            options {
                timeout(time: 3, unit: "SECONDS")
            }

            steps {
                catchError(buildResult: 'SUCCESS', stageResult: 'ABORTED') { (1)
                    echo "Started stage A"
                    sleep(time: 5, unit: "SECONDS")
                }
            }
        }

        stage("B") {
            steps {
                echo "Started stage B"
            }
        }
    }
}

In this example, we wrapped steps that potentially may timeout with the catchError . We want to mark the stage as ABORTED, but we want to keep the build status as SUCCESS. This way, we don’t want to affect the final build result with the timeout of the A stage. Let’s look at what happens when we run the pipeline now.

Looks good, but there is one small issue. We can see that stage B was executed, however, the job status is set to ABORTED. It turns out that, according to the documentation, we cannot set the build result to SUCCESS from results like UNSTABLE or ABORTED.

If an error is caught, the overall build result will be set to this value. Note that the build result can only get worse, so you cannot change the result to SUCCESS if the current result is UNSTABLE or worse. Use SUCCESS or null to keep the build result from being set when an error is caught.

Using try-catch inside script step

Let’s see if replacing catchError with good old try-catch can help us in this situation.

Listing 3. Jenkinsfile
pipeline {
    agent any

    stages {
        stage("A") {
            options {
                timeout(time: 3, unit: "SECONDS")
            }

            steps {
                script { (2)
                    try {
                        echo "Started stage A"
                        sleep(time: 5, unit: "SECONDS")
                    } catch (Throwable e) {
                        echo "Caught ${e.toString()}"
                        currentBuild.result = "SUCCESS" (1)
                    }
                }
            }
        }

        stage("B") {
            steps {
                echo "Started stage B"
            }
        }
    }
}

In this example, we want to catch any potential exception and force the SUCCESS build status . Keep in mind, that we had to put the code into the script step to be able to use try-catch block. Let’s run the pipeline and see how it goes.

We can see the expected build status, but there is one issue with this approach. The A stage is marked as SUCCESS which might be confusing. It hides the information about the timeout, so we need to click on the stage and check if there was a timeout or not every time we run the pipeline. There should be a better way to handle it.

Using catchError together with try-catch

Luckily, there is a better solution. We can combine both catchError with the try-catch. Let’s take a look at the final example.

Listing 4. Jenkinsfile
pipeline {
    agent any

    stages {
        stage("A") {
            options {
                timeout(time: 3, unit: "SECONDS")
            }

            steps {
                script {
                    Exception caughtException = null

                    catchError(buildResult: 'SUCCESS', stageResult: 'ABORTED') { (1)
                        try { (2)
                            echo "Started stage A"
                            sleep(time: 5, unit: "SECONDS")
                        } catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException e) {
                            error "Caught ${e.toString()}" (3)
                        } catch (Throwable e) {
                            caughtException = e
                        }
                    }

                    if (caughtException) {
                        error caughtException.message
                    }
                }
            }
        }

        stage("B") {
            steps {
                echo "Started stage B"
            }
        }
    }
}

In this example, we use catchError to control the stage result in case of an error . The code that may potentially timeout is wrapped with the try-catch so we can control the exception. We can be very specific - in this case we catch the FlowInterruptedException to mark the current stage as ABORTED , but we also store any other exception as caughtException. If any exception other than FlowInterruptedException occurs, we execute error step with an exception message to fail the A stage. Let’s run and see the final result.

And here is what happens if the code that may timeout throws a different error (for instance, some shell command may return exit code 1.)

Voilà! Now it works just as we expect. The A stage gets executed, and it is acceptable to timeout. The pipeline continues in such a case, and its final result depends on the remaining stages.

Share this blog post on Twitter or LinkedIn to help me spread the word. Thanks!