# 09 · Organize Scripts and Handle Errors

English narration · HHY 1.7.0

Export a local loader; compare catch, per-item Result, and fatal failure.

## 00:00:00.000 — Separate reusable work from orchestration

As scripts grow, repeating parsing and normalization code becomes difficult to maintain. In this lesson, a small local module owns that reusable work, while main decides which inputs to process and what failures mean. We will run one valid file and one deliberately broken file. The successful result should remain visible, the failure should remain visible, and the batch should finish only because we intentionally chose to handle it.

```
hhy run main.hhy
```

## 00:00:32.680 — Export a focused function

Our names module exports a function that trims a name and converts its letters to lowercase. Export marks this function as part of the module's public interface. Helpers without export stay private to the module. Keep functions focused so callers can understand their input and result without knowing the internal file layout. This normalization function does not read files, which also makes it easy to demonstrate with a literal test value.

```
export fn normalize_name(name) {
    return name |> trim |> lower
}
```

## 00:01:05.570 — Compose the file loader

Load_name combines three responsibilities in an explicit sequence: read text, parse JSON, and require the name field before normalization. Require expresses that the field must exist. A missing required field should not accidentally flow onward as a successful empty name. This lesson keeps the schema small; applications may also need to validate the field's type and business rules before normalization. Failures propagate to the caller until somebody handles them.

```
export fn load_name(filename) {
    let value = read_text(path(filename)) |> parse_json
    return normalize_name(require(value, "name"))
}
```

## 00:01:41.620 — Import names from a relative source path

The import path is resolved relative to the source file containing the import. This differs from the data filename passed into our loader, which is resolved from the process working directory. Run from examples as the README instructs. The module is loaded and cached, so ordinary repeated imports do not repeatedly execute its top level. Choose explicit exports and small modules before building a complicated directory hierarchy.

```
import { load_name } from "./lib/names.hhy"
import { normalize_name } from "./lib/names.hhy"
print(normalize_name("  HHY  "))
print(load_name("fixtures/good.json"))
```

Expected output:
```text
hhy
ada
```

## 00:02:13.770 — Catch a failure at a useful boundary

The broken fixture is not valid JSON. Catch receives the Error that propagates out of load_name, and our demonstration prints its category. The observed category here is ValueError. Error values also carry diagnostic information such as code and message. Handle errors where you have enough context to decide what to do. A catch block that merely hides a failure can make an unsuccessful operation look deceptively complete.

```
try {
    load_name("fixtures/bad.json")
} catch err {
    print("caught invalid input")
    print(err.kind)
}
```

Expected output:
```text
caught invalid input
ValueError
```

## 00:02:45.940 — Make per-item outcomes explicit

An unhandled error in a stream normally ends that pipeline. Attempt changes the contract for one operation: it returns a Result representing success or failure. That lets our batch retain an outcome for each filename instead of losing the whole demonstration at the broken file. Notice the explicit stream conversion on the List. Also notice that attempt belongs inside the callback, so each input gets its own individual outcome.

```
let inputs = ["fixtures/good.json", "fixtures/bad.json"]
inputs |> stream
    |> map { filename ->
        attempt { load_name(filename) }
    }
```

## 00:03:17.900 — Report success and failure separately

Check result dot ok before accessing the branch specific value. A successful Result provides value; a failed Result provides error. We print both branches, so the report does not silently discard bad input. Production reports should usually include the input identity alongside each outcome. Runtime cleanup still applies when operations fail, but it cannot choose your business policy. You must decide whether partial success is acceptable for the job.

```
    |> for_each { result ->
        if result.ok {
            print("success: " + result.value)
        } else {
            print("failure: " + result.error.kind)
        }
    }
```

Expected output:
```text
success: ada
failure: ValueError
```

## 00:03:52.030 — Compare with an unhandled failure

Run the separate fail script to see the other policy. It calls the same loader on the broken fixture without catch or attempt, so the command exits nonzero. That is useful when a job must not continue after invalid input. The reusable module stays unchanged; orchestration determines whether errors are fatal or collected. Avoid catching everything at the deepest function merely to prevent an error from reaching its caller.

```
hhy run fail.hhy
```

## 00:04:23.900 — Practice an explicit failure policy

For practice, add a valid JSON fixture that lacks the name field. Predict whether parsing or require will reject it, then include it in the batch and inspect its error category. Next, add the filename to each printed outcome so the report is actionable. Keep fail as a separate example of a fatal policy. You now have reusable modules, a clear import rule, and two deliberate ways to handle unsuccessful work.

```
# Run both policies from the examples directory.
hhy run main.hhy
hhy run fail.hhy
```

