# Errors and Fallbacks

Course: [AI Agents](https://www.devtutorials.ai/learn/ai-agents) · Section: Agent Resilience
Source: https://www.devtutorials.ai/learn/ai-agents/resilience/errors-and-fallbacks

> How an agent notices tool errors, bad output and runtime faults, recovers with retries and other actions, and falls back to partial answers or escalation instead of returning nothing.

Think about a delivery driver with a parcel to drop off.

The van won't start. The driver tries the key again. Sometimes that is all it takes.

If it still won't start, the driver doesn't give up. They borrow a bike. If the address is closed, they leave the parcel at the shop next door and put a note in the letterbox explaining where it is.

If none of that works, they call the office and explain exactly what happened. What a good driver never does is drive home and say nothing.

An agent should handle problems in the same way.

**The main idea:** when something goes wrong, an agent should notice it, identify it, try to recover, and check whether the recovery worked. If recovery fails, it should fall back to a smaller but honest result. It should never return nothing.

## A Real-World Example

The Brightgrid agent runs the tests. Instead of a test result, it receives: `ERROR: the run_tests runner crashed`.

A weak agent might treat that as a test failure and start changing code. Or it might stop immediately.

A strong agent recognizes that this is a tool failure, not a test result. It might be temporary, so it tries again.

If the second attempt works, the agent continues normally. If the test runner keeps crashing, the agent gives an honest partial answer: "I changed `web/format.js`, but I could not run the tests. Please run them before you merge."

**In simple words:** notice, retry, try something else, and if all else fails, clearly explain what you could and could not do.

## What You Will Learn

In this lesson, you will learn:

1. The three kinds of fault: tool errors, invalid outputs, and runtime failures.
2. How to recover from failures using retries, parameter fixes, and alternative actions.
3. How to verify that a recovery actually worked.
4. How fallback chains provide alternatives when recovery fails.
5. How graceful degradation delivers a smaller but safe result and escalates when necessary.

The **Agent Lab** injects faults into the `run_tests` tool in this lesson. The **Loop** tab shows which fault is active. Every run uses the same agent, with retries and a fallback chain enabled.

## Error Detection

The first job is to **notice** a fault and identify its type. The type determines the next action.

### Tool Errors

A **tool error** means the tool itself failed. It crashed or returned an error. The result starts with `ERROR`.

### Invalid Outputs

An **invalid output** means the tool reported success, but the output is wrong. It may be empty, incomplete, or in the wrong format. You saw output validation in Lesson 11.

### Runtime Failures

A **runtime failure** comes from the environment around the tool. The network is slow, the call timed out, or the machine ran out of memory. These faults are often temporary.

```js runnable title="Name the kind of fault"
function faultKind(result) {
  if (/crash/i.test(result)) return "tool error";
  if (/timed out/.test(result)) return "runtime fault";
  if (!/\d+ passed, \d+ failed/.test(result)) return "bad output";
  return "no fault";
}

console.log(faultKind("ERROR: the run_tests runner crashed"));
console.log(faultKind("ERROR: timed out after 5 s"));
console.log(faultKind(""));
console.log(faultKind("2 passed, 1 failed"));
```

Expected output:

```text
tool error
runtime fault
bad output
no fault
```

The last one has a failing test, but that is *not* a fault. A failing test is real information about the code. A fault means the agent learned nothing useful about the code.

## Recovery Strategies

### Retry Logic

A **retry** runs the same action again. It is the right first response for faults that may be temporary, such as crashes, timeouts, and empty results.

Two rules make retries safe:

1. **Limit them.** Try only two or three times, then stop retrying.
2. **Wait a little longer each time.** This is called **backoff**. It gives a busy service time to recover.

```js runnable title="Retry with a limit and backoff"
let calls = 0;
function runTests() {
  calls++;
  if (calls <= 2) return "ERROR: timed out";
  return "3 passed, 0 failed";
}

function withRetry(fn, maxTries) {
  let wait = 100;
  for (let attempt = 1; attempt <= maxTries; attempt++) {
    const r = fn();
    console.log("try " + attempt + ": " + r);
    if (!r.startsWith("ERROR")) return r;
    console.log("  wait " + wait + " ms");
    wait *= 2;
  }
  return "GAVE UP";
}

console.log("result: " + withRetry(runTests, 3));
```

Expected output:

```text
try 1: ERROR: timed out
  wait 100 ms
try 2: ERROR: timed out
  wait 200 ms
try 3: 3 passed, 0 failed
result: 3 passed, 0 failed
```

The test run times out twice, and the third attempt succeeds. Each timeout costs 5 seconds of waiting, so this run takes 21.9 seconds in the Budget tab. That is why retries need a limit.

### Parameter Adjustment

Sometimes retrying the *same* call is pointless because the call itself is wrong. **Parameter adjustment** fixes the inputs first. You saw this in Lesson 11, when the agent read the error "write_file needs path and content" and corrected the argument names.

```js runnable title="Fix the inputs, then retry"
function writeFile(args) {
  const missing = ["path", "content"].filter((k) => !(k in args));
  return missing.length ? "ERROR: write_file needs " + missing.join(" and ") : "Saved " + args.path;
}

let args = { file: "web/format.js", text: "..." };
let result = writeFile(args);
console.log("try 1: " + result);

if (result.includes("needs path and content")) args = { path: args.file, content: args.text };
console.log("try 2: " + writeFile(args));
```

Expected output:

```text
try 1: ERROR: write_file needs path and content
try 2: Saved web/format.js
```

Retrying try 1 unchanged would fail forever. Reading the error and fixing the inputs makes try 2 work.

### Alternative Actions

An **alternative action** reaches the same goal in a different way. For example: if the full test run keeps timing out, run only the one test file that matters.

The first test run says ok but returns nothing. The agent does not treat "nothing" as "no failures". It identifies the result as bad output and retries.

### Recovery Verification

After recovering, **verify** it. Run the check again, or examine the new result carefully. A retry that "worked" but returned strange output is not a recovery.

In the runs above, the agent's next thought says, "The retry worked, so the fault was temporary." It says that only after receiving a real test result.

## Fallback Mechanisms

A **fallback** is a plan B: something smaller that still helps when plan A cannot succeed.

### Secondary Actions

A **secondary action** is a simpler action toward the same goal. For example: if the full fix cannot be checked, write the fix and list the exact command a person can run to verify it.

### Alternate Tools

An **alternate tool** performs a similar job. If `run_tests` is unavailable, `start_tests` and `check_job` from Lesson 11 might still work.

### Partial Solutions

A **partial solution** explains what was done and clearly states what was not done.

A good partial answer always has two parts: what *was* done, and what is *not* checked.

The fix is written at step 3. Then the test runner fails permanently.

The agent retries twice and then stops retrying. Its final answer begins with **PARTIAL**: it identifies the changed file and states that the change has not been checked.

```js runnable title="A fallback chain that never returns nothing"
function attempt(plan) {
  const steps = [
    () => plan.testsWork ? "DONE: fixed and tested" : null,
    () => plan.altToolWorks ? "DONE: fixed and tested with the background test tool" : null,
    () => plan.fixWritten ? "PARTIAL: fix written, not tested. Please run the tests." : null,
    () => "ESCALATE: nothing changed. A person must fix the test runner.",
  ];
  for (const step of steps) {
    const r = step();
    if (r) return r;
  }
}

console.log(attempt({ testsWork: true }));
console.log(attempt({ testsWork: false, altToolWorks: true }));
console.log(attempt({ testsWork: false, altToolWorks: false, fixWritten: true }));
console.log(attempt({ testsWork: false, altToolWorks: false, fixWritten: false }));
```

Expected output:

```text
DONE: fixed and tested
DONE: fixed and tested with the background test tool
PARTIAL: fix written, not tested. Please run the tests.
ESCALATE: nothing changed. A person must fix the test runner.
```

The last link has no condition, so the chain always returns *something*.

## Graceful Degradation

**Graceful degradation** means doing less, but doing it safely, when parts of the system break. The opposite is a crash or a silent failure.

### Reduced Functionality

**Reduced functionality** keeps the safe parts working. A review agent with a broken test runner can still read the code and comment on it. It just cannot say "the tests pass".

### Safe Responses

A **safe response** never claims more than the agent actually knows. "Fixed" is only safe if the tests ran and passed. Otherwise, the answer must clearly say "not checked".

```js runnable title="Never claim more than you checked"
function report(r) {
  if (r.testsRan && r.testsPass) return "Fixed. All tests pass.";
  if (r.changed.length) return "Changed " + r.changed.join(", ") + ". NOT checked: " + r.why + ".";
  return "Nothing changed: " + r.why + ".";
}

console.log(report({ testsRan: true, testsPass: true, changed: ["web/format.js"] }));
console.log(report({ testsRan: false, changed: ["web/format.js"], why: "the test runner is down" }));
console.log(report({ testsRan: false, changed: [], why: "the test runner is down" }));
```

Expected output:

```text
Fixed. All tests pass.
Changed web/format.js. NOT checked: the test runner is down.
Nothing changed: the test runner is down.
```

The word "Fixed" should appear only when the tests ran and passed.

### Escalation Paths

**Escalation** hands the problem to a person, together with everything they need to act: what failed, what was tried, and what was changed.

The test runner never works. The agent has changed nothing, so a partial answer would contain no useful result. Instead, it escalates and clearly states that no changes were made.

**Easy Way to Remember:** **name it, retry it, route around it, report it.**

Never turn a fault into a confident answer. "The tests didn't run" is not the same as "the tests passed", and the final answer must clearly state which one happened.

## Common Mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Fault read as failure | "An error means the tests failed." | Name it: tool error, bad output, or runtime fault. | The agent "fixes" code that was never broken. |
| Endless retries | "It will work eventually." | Retry 2 or 3 times, then fall back. | The run wastes time and money on a broken tool. |
| Same call, same mistake | "Just try again." | Fix the inputs when the error says they are wrong. | Every retry fails in the same way. |
| Confident partial work | "I wrote the fix, so it's fixed." | Say exactly what is not checked. | Untested code gets merged. |
| Silent giving up | "I couldn't, so I'll stop." | Escalate with what failed and what was tried. | Nobody knows the task is stuck. |

## Exercises

Each exercise ends with a value on the last line. Press **Check** to compare it with the expected answer.

## What to Remember

- **Name it, retry it, route around it, report it.**
- There are three kinds of faults: **tool errors**, **bad output**, and **runtime faults**.
- Retry temporary faults with a limit and backoff. Fix the inputs when the call itself is wrong.
- Verify every recovery with a real result.
- End every run with one of three outcomes: **done**, **partial**, or **escalate**. Never end with nothing.

Next: **Loop Protection**, the guards that stop an agent from going round in circles.
