I know this question is a few years old but would like to offer an alternative for people who are coming across this problem. The accepted answer is close to what I needed, but the issue is that catchError does not allow for an alternate set of code to execute when an error occurs in its block like a try/catch does. I got around this in the following way:
stage('Stage 2') { steps { echo 'In stage 2' script { try { echo 'In stage 2: try block' sh 'python3 ./python/script.py' } catch(Exception e) { echo 'In stage 2: catch block' catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') { echo 'In stage 2: catchError block' sh 'exit 1' } } } } }
When my Python script raises an exception, whatever is in the catch block will execute per usual try/catch logic. Putting the catchError there as well and raising a bogus exception in it guarantees that my build and stage statuses will come out how I want (e.g. all stages with status = SUCCESS except for Stage 2, and a build status = UNSTABLE).
There's no question that this is an awkward workaround. However, the Jenkins developers insist (see here) that "To ensure consistency the FAILED state should always fail the pipeline."
My code snippet above is just simple proof of concept, but in my production code I have a legitimate need to fail a stage, yet mark the entire build as UNSTABLE. The noble intentions behind designing Jenkins declarative pipelines with so much rigor are admirable, but really inconvenient and unnecessary in my opinion.
Hopefully this workaround helps someone. If I'm missing something then I'm open to looking at a cleaner way of doing this.