diff --git a/setup.py b/setup.py
index 68cd7fc..e20f87c 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
 from setuptools import setup
 
 setup(name='slurm-pipeline',
-      version='1.1.8',
+      version='1.1.9',
       packages=['slurm_pipeline'],
       include_package_data=True,
       url='https://github.com/acorg/slurm-pipeline',
diff --git a/slurm_pipeline/pipeline.py b/slurm_pipeline/pipeline.py
index e8c6eaf..443c1ae 100644
--- a/slurm_pipeline/pipeline.py
+++ b/slurm_pipeline/pipeline.py
@@ -281,11 +281,28 @@ class SlurmPipeline(SlurmPipelineBase):
         @param step: A C{dict} with a job specification.
         @param env: A C{str} key to C{str} value environment for the script.
         @param args: A C{list} of command-line arguments.
-        @raise SchedulingError: If a script outputs a task name more than once.
+        @raise SchedulingError: If a script outputs a task name more than once
+            or if the step script cannot be executed.
         """
-        step['stdout'] = subprocess.check_output(
-            [step['script']] + args, cwd=step.get('cwd', '.'), env=env,
-            stdin=DEVNULL, universal_newlines=True)
+        try:
+            step['stdout'] = subprocess.check_output(
+                [step['script']] + args, cwd=step.get('cwd', '.'), env=env,
+                stdin=DEVNULL, universal_newlines=True)
+        except subprocess.CalledProcessError as e:
+            import sys
+            if sys.version_info >= (3, 5):
+                raise SchedulingError(
+                    "Could not execute step '%s' script '%s' in directory "
+                    "'%s'. Attempted command: '%s'. Exit status: %s. Standard "
+                    "output: '%s'. Standard error: '%s'." % (
+                        step['name'], step['script'], step.get('cwd', '.'),
+                        e.cmd, e.returncode, e.output, e.stderr))
+            else:
+                raise SchedulingError(
+                    "Could not execute step '%s' script '%s' in directory "
+                    "'%s'. Attempted command: '%s'. Exit status: %s."
+                    % (step['name'], step['script'],  step.get('cwd', '.'),
+                       e.cmd, e.returncode))
 
         # Look at all output lines for task names and SLURM job ids created
         # (if any) by this script. Ignore any non-matching output.

Improve error output when running step scripts
Catch a subprocess error and print the stdout and stderr of the offending script.
diff --git a/test/test_pipeline.py b/test/test_pipeline.py
index 4c85b14..a392015 100644
--- a/test/test_pipeline.py
+++ b/test/test_pipeline.py
@@ -3,6 +3,8 @@ from unittest import TestCase
 from six import assertRaisesRegex
 from json import dumps
 import platform
+from subprocess import CalledProcessError
+from sys import version_info
 
 from slurm_pipeline.pipeline import SlurmPipeline, DEVNULL
 from slurm_pipeline.error import SchedulingError, SpecificationError
@@ -797,7 +799,8 @@ class TestSlurmPipeline(TestCase):
     @patch('subprocess.check_output')
     @patch('os.access')
     @patch('os.path.exists')
-    def testCwdWithRelativeScriptPath(self, existsMock, accessMock,
+    @patch('os.path.isdir')
+    def testCwdWithRelativeScriptPath(self, isdirMock, existsMock, accessMock,
                                       subprocessMock):
         """
         If a step has a cwd set and its script is a relative path, the path of
@@ -1682,3 +1685,41 @@ class TestSlurmPipeline(TestCase):
                  "\[-10000, 10000\] range$")
         assertRaisesRegex(self, SchedulingError, error, sp.schedule,
                           nice=-10001)
+
+    @patch('subprocess.check_output')
+    @patch('os.access')
+    @patch('os.path.exists')
+    @patch('os.path.isdir')
+    def testSubprocessExecRaises(self, isdirMock, existsMock, accessMock,
+                                 subprocessMock):
+        """
+        If subprocess.check_output raises CalledProcessError, we must
+        raise a corresponding SchedulingError.
+        """
+        sp = SlurmPipeline(
+            {
+                'steps': [
+                    {
+                        'cwd': 'dir',
+                        'name': 'name1',
+                        'script': 'script1',
+                    },
+                ],
+            })
+
+        if version_info >= (3, 5):
+            subprocessMock.side_effect = CalledProcessError(
+                3, 'command.sh', output='the stdout', stderr='the stderr')
+
+            error = ("^Could not execute step 'name1' script 'script1' in "
+                     "directory 'dir'\. Attempted command: 'command.sh'\. "
+                     "Exit status: 3\. Standard output: 'the stdout'\. "
+                     "Standard error: 'the stderr'\.$")
+        else:
+            subprocessMock.side_effect = CalledProcessError(3, 'command.sh')
+
+            error = ("^Could not execute step 'name1' script 'script1' in "
+                     "directory 'dir'\. Attempted command: 'command.sh'\. "
+                     "Exit status: 3\.$")
+
+        assertRaisesRegex(self, SchedulingError, error, sp.schedule)

pip install -e .
parse_log_pytest
requirements.txt
discover pytest
apt-get update apt-get install -y gcc
3.9
requirements-3.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
discover==0.4.0
exceptiongroup==1.2.2
iniconfig==2.1.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
six==1.10.0
-e git+https://github.com/acorg/slurm-pipeline.git@2aec1fd60f0765a26555768ec15719dcf973f294#egg=slurm_pipeline
tomli==2.2.1

name: slurm-pipeline
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - discover==0.4.0
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - six==1.10.0
      - tomli==2.2.1
prefix: /opt/conda/envs/slurm-pipeline

test/test_pipeline.py::TestSlurmPipeline::testSubprocessExecRaises
test/test_pipeline.py::TestSlurmPipeline::testAccessAndExistsAreCalled test/test_pipeline.py::TestSlurmPipeline::testAccessAndExistsAreCalledWithAbsolutePathScript test/test_pipeline.py::TestSlurmPipeline::testAccessAndExistsAreCalledWithCwd test/test_pipeline.py::TestSlurmPipeline::testCollectStepWithEmptyDependencies test/test_pipeline.py::TestSlurmPipeline::testCollectStepWithNoDependencies test/test_pipeline.py::TestSlurmPipeline::testCwdWithRelativeScriptPath test/test_pipeline.py::TestSlurmPipeline::testDefaultNice test/test_pipeline.py::TestSlurmPipeline::testErrorStep test/test_pipeline.py::TestSlurmPipeline::testErrorStepWithNoDependencies test/test_pipeline.py::TestSlurmPipeline::testFirstStepAndLastStepDifferent test/test_pipeline.py::TestSlurmPipeline::testFirstStepAndLastStepSame test/test_pipeline.py::TestSlurmPipeline::testFirstStepAndNoLastStep test/test_pipeline.py::TestSlurmPipeline::testFirstStepOnly test/test_pipeline.py::TestSlurmPipeline::testForce test/test_pipeline.py::TestSlurmPipeline::testJSON test/test_pipeline.py::TestSlurmPipeline::testLastStepBeforeFirstStep test/test_pipeline.py::TestSlurmPipeline::testLastStepOnly test/test_pipeline.py::TestSlurmPipeline::testNiceTooBig test/test_pipeline.py::TestSlurmPipeline::testNiceTooSmall test/test_pipeline.py::TestSlurmPipeline::testNonexecutableScript test/test_pipeline.py::TestSlurmPipeline::testNonexistentDir test/test_pipeline.py::TestSlurmPipeline::testNonexistentFirstStep test/test_pipeline.py::TestSlurmPipeline::testNonexistentLastStep test/test_pipeline.py::TestSlurmPipeline::testNonexistentScript test/test_pipeline.py::TestSlurmPipeline::testRepeatedTaskJobId test/test_pipeline.py::TestSlurmPipeline::testRepeatedTaskName test/test_pipeline.py::TestSlurmPipeline::testScheduledTime test/test_pipeline.py::TestSlurmPipeline::testScriptArgs test/test_pipeline.py::TestSlurmPipeline::testSingleCollectorDependencyNoJobIds test/test_pipeline.py::TestSlurmPipeline::testSingleCollectorDependencyTaskNamesAndJobIds test/test_pipeline.py::TestSlurmPipeline::testSingleDependencySynchronousTaskNamesJobIdsAndCalls test/test_pipeline.py::TestSlurmPipeline::testSingleDependencyTaskNamesJobIdsAndCalls test/test_pipeline.py::TestSlurmPipeline::testSkipNone test/test_pipeline.py::TestSlurmPipeline::testSkipNonexistentStep test/test_pipeline.py::TestSlurmPipeline::testSkipNonexistentSteps test/test_pipeline.py::TestSlurmPipeline::testSkipTwo test/test_pipeline.py::TestSlurmPipeline::testSleep test/test_pipeline.py::TestSlurmPipeline::testSleepNotCalledByDefault test/test_pipeline.py::TestSlurmPipeline::testSleepNotCalledWhenZero test/test_pipeline.py::TestSlurmPipeline::testSpecificNice test/test_pipeline.py::TestSlurmPipeline::testStartAfter test/test_pipeline.py::TestSlurmPipeline::testStepStdout test/test_pipeline.py::TestSlurmPipeline::testStepsDict test/test_pipeline.py::TestSlurmPipeline::testStringNice test/test_pipeline.py::TestSlurmPipeline::testTaskScheduleTime test/test_pipeline.py::TestSlurmPipeline::testTasksFollowingSchedule
setup.py slurm_pipeline/pipeline.py
setup.py slurm_pipeline/pipeline.py