Skip to content

Include the failing path in Path errors - #468

Open
dmisiuk wants to merge 1 commit into
roc-lang:mainfrom
dmisiuk:issue-399-path-error-context
Open

Include the failing path in Path errors#468
dmisiuk wants to merge 1 commit into
roc-lang:mainfrom
dmisiuk:issue-399-path-error-context

Conversation

@dmisiuk

@dmisiuk dmisiuk commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Draft for #399Path errors now carry the path that failed.

Shape agreed with @Anton-4 on Zulip: positional payload, no operation field for now, and File.open_reader! enriched in this PR too.

The change

Today every Path effect returns PathErr(IOErr), so a failure gives no clue which path — or which of several calls — failed:

Program exited with error: PathErr(NotFound)

The path is already in hand at the failure point, so this is pure Roc in Path.roc — no host or glue changes. The two converters take the path and put it in the error:

map_file_result : Try(a, [FileErr(IOErr)]), Path -> Try(a, [PathErr(IOErr, Path), ..])
Program exited with error: PathErr(NotFound, Path.unix("missing.txt"))

That rendering is free — main_for_host! prints via Str.inspect and Path already has to_inspect.

Why positional rather than a record

Sqlite.roc already uses a positional payload for exactly this shape (SqliteErr(ErrCode, Str)), and it keeps the common match a one-liner:

Err(PathErr(NotFound, _)) => Ok(Bool.False)

The platform's own is_file! / is_dir! / is_sym_link! / exists! are written that way, and they stay one-liners here. With a record payload they would each become a full destructure — there are no partial record patterns anywhere in the repo. The record form (like Cmd.FailedToGetExitCode({ command, err })) is the alternative under discussion.

Breaking change — narrow

?, ? |err| …, ?? and Err(_) all keep working, since the error type is open and inferred. Only an explicit destructure of the inner tag breaks:

  • 4 spots in Path.roc itself (is_file! and friends), now Err(PathErr(NotFound, _))
  • 6 lines across examples/error-handling.roc and examples/file-accessed-modified-created-time.roc
  • 1 assertion in scripts/test_spec.json that matched the rendered error text

That last one is a category I'd missed until the suite caught it — worth noting for whoever reviews the blast radius.

Notes for review

  • examples/error-handling.roc now demonstrates both styles: naming the path in the message, and _ when it adds nothing.
  • The file-size.roc assertion asserts the PathErr(NotFound, Path. prefix rather than the whole value: that case also runs on Windows, where the path renders as Path.windows(...) instead of Path.unix(...). This matches how other inspect-rendered errors are asserted (VarNotFound(OsStr., Debug: Path.).
  • hard_link! and rename! both attribute the error to their source operand (original / from), which is what a missing-file failure refers to; some failures concern the destination instead (AlreadyExists), so both carry a doc note. I had hard_link! on the destination at first, which made hard_link!("missing.txt", "new") report NotFound against the file being created — actively misleading, so it's fixed here.
  • operation is deliberately not included, per the thread — easy to add later if it turns out to be wanted.

Three expect blocks cover the converters: the path survives into the error (including raw non-UTF-8 bytes), and Ok passes through. Each was verified to fail when its expected value is altered. Note these have to be match-based rather than ==, since IOErr doesn't support equality — that's pre-existing, not introduced here.

Full ./scripts/test.py passes — host build plus every example.

File.open_reader!

It takes a Path and used to return a bare FileErr(err), so whether an open failure named the path depended on which module you called. It now reports PathErr(err, path) — the same tag as the whole-file operations rather than a second one, because the two shapes have to unify: File.Reader.read_line! keeps returning FileErr(IOErr) (a Reader carries no path), and the compiler rejects one tag name used at two arities, so chaining an open with a read would not type-check otherwise. Using PathErr also means an open and a Path operation collapse into a single error type in one ? chain.

@dmisiuk
dmisiuk force-pushed the issue-399-path-error-context branch from 46d9b26 to 18c8d0d Compare August 2, 2026 17:49
@dmisiuk
dmisiuk marked this pull request as ready for review August 2, 2026 18:03
@dmisiuk
dmisiuk force-pushed the issue-399-path-error-context branch from 18c8d0d to 3779aea Compare August 3, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant