# Null means miss, not failure

**Effort:** S · **Impact:** M · **Immediate roc UX:** yes

## Repro (sub-par today)

1. Open a Roc buffer in an editor using `roc experimental-lsp`.
2. Hover (or go-to-definition) on a real identifier while the checker path can fail for a **non-OOM** reason — e.g. provoke a `getTypeAtPosition` / `getDefinitionAtPosition` error other than “no symbol here” (build/session failure that surfaces as an error union, not `null`).
3. Observe editor UX: empty hover / “no definition” — **identical** to hovering whitespace or an unknown token.
4. Server log may show `hover failed: …` / similar, but the wire reply is still `"result": null`.

**Concrete code path (always true today):**

```zig
// src/lsp/handlers/hover.zig
const hover_result = self.syntax_checker.getTypeAtPosition(...) catch |err| switch (err) {
    error.OutOfMemory => return error.OutOfMemory,
    else => {
        std.log.err("hover failed: {s}", .{@errorName(err)});
        try self.sendNullResponse(id);  // ← looks like “nothing here”
        return;
    },
};
```

Same pattern in `src/lsp/handlers/definition.zig`.

## Evidence

- `src/lsp/handlers/hover.zig` → `call` (`catch … sendNullResponse`)
- `src/lsp/handlers/definition.zig` → `call` (same)
- Legitimate miss already handled by the `else` branch after a successful `null` Optional — keep that.

## Goal

LSP `null` result = “query succeeded; nothing at this position.”  
Hard failure = JSON-RPC `error` (e.g. `InternalError`) so the client / logs / status can show something went wrong.

## Fix shape

1. On non-OOM catch in hover/definition (and any peer that copies the pattern): `sendError(id, .internal_error, …)` instead of `sendNullResponse`.
2. Keep `sendNullResponse` only when the checker returns a successful empty Optional.
3. Optionally share a tiny helper: `respondQueryResult(id, ?T)` vs `respondQueryErr(id, err)`.
4. Test: force non-OOM error from a test SyntaxDriver → assert error response, not null result.

## Acceptance

- [ ] Successful miss → `result: null`.
- [ ] Checker error → JSON-RPC error with matching `id` (not null).
- [ ] OOM still bubbles / is handled consistently with other handlers.

## Out of scope

- Changing what counts as a CIR miss vs hit (see `05-delete-agents-fallbacks.md`).
- Top-level `handlePayload` swallow (see `02-never-drop-request-replies.md`) — complementary, do both.
