Imagine you ask a friend to read a letter out loud, but the envelope is glued shut, or addressed to the wrong house, or written in a language nobody in the room reads. Your friend never gets to read a single word. That failure has nothing to do with what the letter says.
status=203/EXEC is exactly that: systemd tried to start your program and could not even open the envelope. The file is not executable (no chmod +x), the path is wrong, the first line #! names an interpreter (the program that should read the script, like bash) that does not exist, or a sandbox setting hides the file. Your program's own logs are empty because it never ran. systemd-analyze verify checks the envelope before you post it.
Why this matters
You write a unit, start it, and get failed with a number: status=203/EXEC. It is the single most common error when writing services, and it has only a handful of causes. Knowing them turns an hour of guessing into a two-minute fix.
What you need to know already: 2.5 (writing a unit, the x bit, the shebang), exit codes (1.7).
What 203 actually means
When systemd starts a service it first prepares the new process (user, folder, settings) and then execs your program - asks the kernel to load that file and run it. If that last step fails, systemd reports the failure with its own exit code, 203, labelled EXEC. In the journal it looks like this:
demo.service: Failed to execute /path/to/script.sh: Permission denied
demo.service: Failed at step EXEC spawning /path/to/script.sh: Permission denied
demo.service: Main process exited, code=exited, status=203/EXEC
Read it bottom-up: the last line is the summary (the main process ended, status 203/EXEC); the lines above say what it tried to run and why it failed (here: Permission denied).
203 is produced before your program ever runs. It means: I could not start that file. Your code has not executed a single line, so there is no point looking for your program's own error messages.
The causes, in the order they actually happen:
Not executable - you forgot chmod +x. (Error: Permission denied.)
Wrong path - a typo, or the file moved. (Error: No such file or directory.)
Missing or wrong shebang - #!/usr/bin/env bash must be the very first line, with no blank line above it. A file saved on Windows fails here too, in a way that looks insane: Windows ends lines with an extra invisible character (\r, "CRLF line endings"), so the kernel looks for a program literally called bash\r.
The interpreter is missing - the program named in the shebang (the interpreter, e.g. python3) is not where the shebang says: #!/bin/python3 when it lives in /usr/bin.
A sandbox setting blocks it - ProtectHome=yes (a setting that hides /home from the service) with a script in /home gives you a Permission denied that no chmod will fix. You will hit this deliberately in 2.27.
Two things that are not 203
status=217/USER - the account named in User= does not exist.
status=200/CHDIR - the folder in WorkingDirectory= does not exist.
Both are the same class of error: the setup failed, not your program.
systemd-analyze is systemd's inspection tool; verify reads a unit file and reports problems without loading it: unknown keys, keys in the wrong section, paths that do not start with /, programs that do not exist. Silence means it found nothing. Run it on anything you have hand-written.
A relative path (one that does not start with /, 1.3) or a ~ in ExecStart is a special case - the unit fails to load at all with bad-setting, and systemctl start refuses before trying:
Failed to start demo.service: Unit demo.service has a bad unit file setting.
systemd does not expand ~. It has no idea whose home you meant.
What you can now do
read a 203/EXEC failure and name its likely cause from the reason text
check a hand-written unit with systemd-analyze verify before starting it
Why it helps
203/EXEC is one of the most frequent systemd failures in real tickets and deploys: a script copied without its execute bit, a file saved on Windows with the wrong line endings, a deploy that wrote the unit but forgot the program, a service moved under ProtectHome. Knowing that codes of 200 and above mean "systemd failed while preparing the process" stops you searching application logs that do not exist.
The reflex behind it - "did it never start, or did it start and crash?" - is one of the best time savers in any incident. The two need completely different next steps, and the exit code tells you which one you are in before you read a single log line.
Why is it Permission denied when the service runs as root?
Running a file needs its execute bit (the x in ls -l), even for root; root skips the read and write checks but not that one. Other causes of the same message: the disk is mounted with noexec (a mount option that forbids running programs from it), a directory on the path cannot be entered by the service's User=, or a sandbox setting like ProtectHome=yes hides the file from that service.
Why does a script saved on Windows fail with such a strange error?
Windows ends each line with an extra invisible carriage-return character. The kernel reads the #! line up to the newline, so it looks for an interpreter called bash plus that invisible character, which does not exist. The error says "No such file or directory" even though the script is right there. file script.sh reports "with CRLF line terminators"; sed -i 's/\r$//' script.sh removes them.
What is the difference between 203/EXEC and a bad-setting error?
bad-setting happens when systemd loads the unit file: it rejects a directive outright, such as a relative path or ~ in ExecStart=, and the unit can never start. systemctl status shows Loaded: bad-setting. 203/EXEC happens when you start it: the unit file is fine, but launching the program fails. Two different moments, two different places to look.
Does systemd-analyze verify catch everything?
No, but it catches a lot for free: unknown or misplaced directives, relative paths, programs that do not exist, and references to units that do not exist. It cannot know about things that only happen when the service really runs, like a sandbox hiding the file, a user that gets created later, or the program crashing. Run it on every unit you write by hand, then still test with systemctl start and status.
How do 217/USER and 200/CHDIR relate to 203?
They are the same family. Codes 200 and above are systemd's own, raised while it prepares the process before your program runs. 217/USER means the User= or Group= does not exist; 200/CHDIR means the WorkingDirectory= is missing or cannot be entered; 203/EXEC means launching the program failed. In all three, fix the unit or the machine, not the application.
In an interview Junior
A service fails with status=203/EXEC. What does it mean, and how do you fix it?
203 is systemd's own code: it prepared the process but could not exec the program in ExecStart=. Your program never ran a single line, so its own logs are empty.
The journal says why, read bottom-up: Failed at step EXEC spawning /path: Permission denied. The usual causes, in order:
not executable - chmod +x;
wrong path or a typo - No such file or directory;
a missing or broken shebang (a blank line above it, or Windows line endings);
the interpreter is not where the shebang says;
a sandbox setting hides it - a script in /home with ProtectHome=yes.
Before reloading a hand-written unit, systemd-analyze verify catches most of these. Its neighbours: 217/USER (the User= does not exist) and 200/CHDIR (no such WorkingDirectory=).
Also asked: What does an exit status of 200 or higher mean in systemctl status? · What does systemd-analyze verify check? · Why does a ~ in ExecStart= give a "bad-setting" error?