Exit Codes: Using $? and && ||

Exit Codes: Using $? and && ||

What you'll be able to do

  • Check whether the last command succeeded or failed with `echo $?`
  • Write "on success" and "on failure" branches with `&&` and `||`
  • Report success or failure to the caller with `exit 0` and `exit 1`

Prerequisites (read these first)

What You'll Learn

Words used here

  • Exit status: the number a command returns when it finishes. Exit code, exit status, and return code all name the same thing.
  • Variable: a named box that holds a value. $? is a special variable the shell fills in for you.
  • Shell script: a file with commands written out in order.

Every command used here -- ls, echo, cd, mkdir -- leaves your existing files alone, so try them freely.

Quick Summary

  • Every command returns a number when it finishes (0 means success, anything else means failure)
  • Want to see the last result? echo $?
  • Run next on success&&
  • Run next on failure||

1. What Is an Exit Status?

Conclusion: An exit status is the number a command returns. 0 means success; 1 to 255 mean failure.

Lina: Senpai, when I run a command the output shows up on screen. But how do I know whether it succeeded or failed?
Linny-senpai: Good question. Every command returns one number when it finishes. We call it the exit status.
Lina: A number? I don't see one on screen.
Linny-senpai: It is hidden from normal view. Think of a delivery slip. Separately from the parcel itself, there is a record saying "delivered" or "nobody home".
Linny-senpai: A command does the same. Apart from its output, it leaves a record. The rule is simple: 0 means success, anything else means failure.

The basic rule

Number Meaning
0 Success
1255 Failure (some problem)

"0 means success" runs against intuition. Lock it in first.

2. Why Does the Exit Status Matter?

Conclusion: The exit status is what lets you automate "run the next step only when this one succeeded."

Lina: All right, a number comes back. But what is it good for?
Linny-senpai: Imagine a task like "delete the old files only after the backup succeeds". Deleting them after a failed backup would be a disaster.
Lina: It would. I'd want the next action to depend on whether the first one worked.
Linny-senpai: Exactly. That decision relies on the exit status. It is the foundation of automation and shell scripting.

Judging success only by what is printed on screen is risky. A command can fail even when no "Error" text appears.

Relying on the machine-readable exit status is far safer.

3. Check the Result with $?

Conclusion: $? holds the exit status of the last command. Use echo $? to see it.

The exit status of the command you just ran is stored in a special variable called $?. The shell fills it in for you.

$ ls /etc
hosts  passwd  ...
$ echo $?
0
Lina: I got 0! So ls succeeded.
Linny-senpai: Right. Now let's make it fail on purpose. Try running ls on a pathA string that describes the location of a file or directory. that doesn't exist.
$ ls /not-exist
ls: cannot access '/not-exist': No such file or directory
$ echo $?
2
Lina: This time I got 2. That is a different number from before.
Linny-senpai: On failure it returns something other than 0. ls returns 2 for a "file not found" error.
Linny-senpai: You do not need to memorize the exact numbers. Just look at whether it is 0 or not.

$? only remembers the result of the immediately preceding command.

Run echo $? twice in a row and the second one reports the first echo. That one succeeded, so you get 0. Check it only once.

4. Chain "on success" with &&

Conclusion: cmd1 && cmd2 runs cmd2 only when cmd1 succeeds (returns 0).

&& is the symbol that means "if the left side succeeds, run the right side too".

$ mkdir backup && cp data.txt backup/
Lina: So this means "run cp only if mkdir succeeded"?
Linny-senpai: Exactly. If mkdir backup fails, cp does not run at all. That happens when you lack permissionThe read / write / execute access rules set on a file or directory. to create the directoryA container that organizes files. Same idea as a "folder" on Windows or macOS., for example.
Linny-senpai: Use it when a step depends on the previous one succeeding.
# Run tests only if the build succeeds
$ make && make test

# List the contents only if the cd succeeds
$ cd /var/log && ls

Think of && as "proceed once the precondition is met". It safely chains steps where you want to stop as soon as something fails.

5. Chain "on failure" with ||

Conclusion: cmd1 || cmd2 runs cmd2 only when cmd1 fails. Use it for error handling.

|| is the opposite of &&. It means "if the left side fails, run the right side".

$ cd /var/log || echo "Could not move into the directory"
Lina: So if cd fails it prints a message. And when it succeeds, nothing is printed?
Linny-senpai: Correct. If cd succeeds, the echo never runs. || is often used as a safety net for when things go wrong.
# Prompt to install if a command is missing
$ which jq || echo "jq is not installed. Please install it."

# Exit the script if the step fails
$ cp data.txt backup/ || exit 1

&& vs || at a glance

cmd1 && cmd2   →  cmd2 runs if cmd1 [succeeds]
cmd1 || cmd2   →  cmd2 runs if cmd1 [fails]

Remember it as "&& proceeds on success" and "|| proceeds on failure".

6. Combining && and ||

Conclusion: cmd && on-success || on-failure writes "do A on success, B on failure" in one line.

Chaining && and || lets you act one way on success and another way on failure.

$ ping -c1 example.com > /dev/null && echo "Connection OK" || echo "Connection NG"
Lina: So I can write "print OK on success, NG on failure" in a single line.
Linny-senpai: Yes, but there is a catch in this pattern.
Linny-senpai: If the success action itself fails -- that is, echo "Connection OK" -- the failure action runs too.
Linny-senpai: It is fine for simple messages. For anything complex, an if statement is safer.

A && B || C is not the same as if A then B else C. If B fails, C also runs.

When you need reliable branching, use an if statement. The next-reading article covers how.

7. Common Beginner Pitfalls

Conclusion: The easy mistakes are getting "0 means success" backwards and reading $? too late.

7-1. Remembering "0 means success" backwards

Lina: Honestly, "0 means success" still feels strange to me.
Linny-senpai: Everyone feels that way. Think of it as "0 = no errors = zero problems" and it sticks.
Linny-senpai: Picture a count of errors, not a test score.

7-2. Lina gets stuck: reading $? too late

$ ls /not-exist
$ pwd            # ← an extra command slipped in
$ echo $?        # this shows pwd's result (0, success)
Lina: The command definitely failed, but I got 0. Is the exit status broken?
Linny-senpai: Nothing is broken. $? only remembers one command back.
Lina: So the pwd I slipped in replaced it?
Linny-senpai: Right. pwd succeeds, so the value became 0. The result from ls was already overwritten.
Lina: The result I wanted was gone. That makes sense. I will read it right after from now on.

Check $? right after the command you care about. Any command in between overwrites it.

7-3. Returning an exit status yourself

When you want a script to report success or failure to its caller, use exit.

exit 0   # end as success
exit 1   # end as failure

If you call exit without a number, it returns the exit status of the last command as-is.

8. Hands-On Exercises

Conclusion: Three exercises — check, run-on-success, run-on-failure — to practice $?, &&, and ||.

Lina: I have the concepts. I want to try it myself.
Linny-senpai: Good. I prepared three exercises. Try them in your terminal.

Exercise 1: Run one command, then print its exit status right after.

Show Hint 1 (Direction)

The last result sits in a special variable the shell fills in. Print its contents on screen.

Show Hint 2 (Command name)

The variable is $?. Print it with echo.

Show Answer
$ ls
$ echo $?
0

ls succeeded, so you get 0. Point it at a directory that does not exist and the number changes to something other than 0.

Exercise 2: Print "Created" only when mkdir test-dir succeeds.

Show Hint 1 (Direction)

Join two commands on one line. Use the symbol that runs the right side only when the left side succeeds.

Show Hint 2 (Command name)

The symbol is &&. Print with echo.

Show Answer
$ mkdir test-dir && echo "Created"
Created

Run the same line a second time and mkdir fails, so "Created" does not appear. Remove the directory afterwards with rmdir test-dir.

Exercise 3: Try to cd into a directory that does not exist, and print "Cannot move" only when it fails.

Show Hint 1 (Direction)

Same shape as Exercise 2, with the symbol reversed. Use the one that runs the right side only when the left side fails.

Show Hint 2 (Command name)

The symbol is ||. Move with cd.

Show Answer
$ cd /not-exist || echo "Cannot move"
bash: cd: /not-exist: No such file or directory
Cannot move

You see both the error message from cd itself and the message you printed.

9. Review

Lina: Let me sum up. A command returns a number when it finishes. 0 is success, anything else is failure.
Linny-senpai: Right. You read that number with echo $?, but only for the one command just before.
Lina: And && proceeds on success while || proceeds on failure. I have that.
Linny-senpai: That's it. Next, try ending one of your own scripts with exit 0 and exit 1.

10. Copy-Paste Templates

Conclusion: Keep the common patterns — check, on-success, on-failure, branch, and exit — within reach.

Common patterns to keep handy

# Check the last exit status
echo $?

# Run next only on success
commandA && commandB

# Run next only on failure
commandA || commandB

# A on success, B on failure (simple branch)
command && echo "OK" || echo "NG"

# Exit the script if the step fails
command || exit 1

# End a script explicitly as success / failure
exit 0   # success
exit 1   # failure

Today's 3-Line Summary

  1. A command returns a number when it finishes. 0 is success, anything else is failure
  2. Read the last result with echo $?, with no other command in between
  3. && proceeds on success. || proceeds on failure

Next Reading

Share this article

Next steps