Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Jenkins By James Joyner IV · · 9 min read

Jenkins Error: 'ERROR: script returned exit code 1' — Cause, Fix, and Troubleshooting Guide

Quick answer

Fix 'ERROR: script returned exit code 1' in Jenkins: read the real sh/bat failure, use set -eo pipefail, returnStatus, and catchError instead of masking it.

  • #jenkins
  • #ci-cd
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

A sh (or bat) step ran a command that exited non-zero, so Jenkins fails the stage and aborts the build. The message is generic on purpose — the interesting failure is in the lines above it:

+ npm run build
... (build output) ...
error TS2304: Cannot find name 'foo'.
ERROR: script returned exit code 1

This is not Jenkins malfunctioning. The sh step reports the exit status of the shell it ran, and any non-zero status fails the step. Your job is to find which command inside the step exited 1, decide whether that failure should fail the build, and either fix the underlying command or handle the exit code deliberately.

Symptoms

  • A stage fails with ERROR: script returned exit code 1 immediately after a sh/bat step.
  • The real error (compiler error, failing test, missing file) is in the console output just above.
  • A multi-line sh step fails even though the “important” command looks like it succeeded.
  • A command that returns a meaningful non-zero code (like grep, diff, test) fails the build unintentionally.
  • The pipeline is red but you want it merely UNSTABLE, or you want to keep going.
  • Works locally, fails in Jenkins — usually because Jenkins runs sh with set -e-like strictness.

Common Root Causes

  • The command genuinely failed — compile error, test failure, lint violation, missing dependency; exit 1 is correct.
  • set -e/pipefail behavior — Jenkins runs sh steps with -e by default, so the first failing command aborts the script; without pipefail a failure mid-pipe can be masked or exposed unexpectedly.
  • Expected non-zero treated as fatal — steps like grep (no match = 1), diff, test, curl -f return non-zero by design.
  • Wrong working directory or missing file — a cd failed earlier, so a later command exits 1.
  • Exit code from the last command in a multi-line step — only the final command’s status (or the first failure under -e) determines the step result.
  • Tool failure vs pipeline failure — a wrapped tool exits 1 but you actually want to record the result and continue.

How to diagnose

The message itself carries no detail — scroll up. The failing command’s own output is what matters. Make the shell tell you exactly what failed by enabling tracing and strict pipe handling:

steps {
  sh '''
    set -euxo pipefail   # -x prints each command; -o pipefail catches mid-pipe failures
    npm ci
    npm run build
  '''
}

To capture the exact exit code without failing the step, use returnStatus:

script {
  def code = sh(script: 'run-tests.sh', returnStatus: true)
  echo "run-tests.sh exited ${code}"
  if (code != 0) { /* decide what to do */ }
}

Reproduce on the agent to isolate tool vs pipeline. From the agent’s workspace shell (or via Manage Jenkins → Nodes → agent-01 → logs):

# On agent-01, in the job workspace
cd /home/jenkins/workspace/my-job
set -euo pipefail
npm run build; echo "exit=$?"

Check the build log around the failure and, for infra-level clues, the agent log via journalctl on the agent host:

journalctl -u jenkins-agent --since '15 min ago' | tail -50

Fixes

1. Fix the underlying command

Most of the time exit 1 is real. Read the error above the message and fix it — a failing test, a compile error, a missing package. Don’t mask a legitimate failure.

2. Make failures explicit and traceable

Add strict mode so the failing command is obvious and mid-pipe failures aren’t hidden:

steps {
  sh 'set -euo pipefail; ./build.sh | tee build.log'
}

Without pipefail, ./build.sh | tee reports tee’s exit code (0) and hides a build failure — a classic false green.

3. Handle expected non-zero exit codes

For commands where non-zero is meaningful, capture the status instead of letting -e abort:

script {
  // grep exits 1 when there is no match — that's not a build failure here
  def hits = sh(script: 'grep -c ERROR app.log', returnStatus: true)
  if (hits == 0) {
    echo 'No ERROR lines found'
  }
}

Or grab output with returnStdout:

script {
  def sha = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
  echo "Building ${sha}"
}

4. Mark the build UNSTABLE instead of FAILED

When a tool failure should flag but not hard-fail the pipeline, use catchError with a stage/build result:

steps {
  catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
    sh 'npm run lint'   // lint problems -> UNSTABLE, pipeline continues
  }
}

For a softer variant that only marks unstable:

steps {
  warnError('Integration tests had failures') {
    sh 'npm run test:integration'
  }
}

5. Distinguish tool failure from pipeline failure

Record real results (JUnit, artifacts) even when the command fails, so you keep evidence:

steps {
  sh(script: 'pytest --junitxml=report.xml', returnStatus: true)
}
post {
  always { junit 'report.xml' }   // publish results regardless of exit code
}

Let the test report — not the raw shell exit — drive the build status.

What to watch out for

  • Always add set -euo pipefail at the top of multi-line sh steps; the default -e alone still hides failures inside pipes.
  • returnStatus: true and returnStdout: true are mutually exclusive on a single sh call.
  • Don’t wrap everything in catchError — masking real failures produces green builds that ship broken code.
  • On Windows agents, bat reports %ERRORLEVEL%; the equivalent strictness differs from POSIX sh — use powershell with $ErrorActionPreference='Stop' for reliable failure propagation.
  • A cd into a missing directory fails the whole step under -e; use dir('subproject') { sh '...' } for clarity.
  • Publish test/lint reports in post { always { ... } } so failures are diagnosable, not just red.
Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.