The gap
THR001 enforces that callers propagate `throws {}` transitively up the call graph. But there is no check on the source side: a leaf fn declaring `throws { X }` without any evidence that its body actually produces `X` is silently accepted. More importantly, the dangerous case is the inverse:
```bs
fn parseConfig(s: string) -> Result<Config, ParseError> {
if (bad) err(NetworkError("timed out")) // NetworkError not declared!
else ok(config)
}
```
The fn returns `Result<Config, ParseError>` but the body constructs `Err(NetworkError(...))`. Callers that match on `ParseError` will never see a `NetworkError` arm — and THR001 won't fire because `parseConfig` doesn't declare `throws { NetworkError }`.
Proposal
```
THR002 fn body constructs err(TypeName(...)) or err(TypeName) where TypeName
is not present in the fn's throws {} set.
```
Mechanically: scan the fn body for `err(` patterns where the first argument is an ident that looks like a constructor (CapCase, followed by `(` or `)`). If that ident is not in the fn's own `throws {}` set (or the transitively declared throws of the fn's callees), fire THR002.
Over-declaration (`throws { X }` declared but X never produced) remains allowed — same policy as THR001 and DEP001/DEP002.
Scope
Token-based detection is reliable for direct construction patterns:
- `err(HttpError(msg))` → detects `HttpError`
- `err(new ParseError(...))` → detects `ParseError`
Indirect patterns (`err(e)` where `e` has a type) require inference and are out of scope.
Relationship to existing diagnostics
Version gate
`?bs 0.9` alongside THR001 — same generation of checks.
The gap
THR001 enforces that callers propagate `throws {}` transitively up the call graph. But there is no check on the source side: a leaf fn declaring `throws { X }` without any evidence that its body actually produces `X` is silently accepted. More importantly, the dangerous case is the inverse:
```bs
fn parseConfig(s: string) -> Result<Config, ParseError> {
if (bad) err(NetworkError("timed out")) // NetworkError not declared!
else ok(config)
}
```
The fn returns `Result<Config, ParseError>` but the body constructs `Err(NetworkError(...))`. Callers that match on `ParseError` will never see a `NetworkError` arm — and THR001 won't fire because `parseConfig` doesn't declare `throws { NetworkError }`.
Proposal
```
THR002 fn body constructs err(TypeName(...)) or err(TypeName) where TypeName
is not present in the fn's throws {} set.
```
Mechanically: scan the fn body for `err(` patterns where the first argument is an ident that looks like a constructor (CapCase, followed by `(` or `)`). If that ident is not in the fn's own `throws {}` set (or the transitively declared throws of the fn's callees), fire THR002.
Over-declaration (`throws { X }` declared but X never produced) remains allowed — same policy as THR001 and DEP001/DEP002.
Scope
Token-based detection is reliable for direct construction patterns:
Indirect patterns (`err(e)` where `e` has a type) require inference and are out of scope.
Relationship to existing diagnostics
Version gate
`?bs 0.9` alongside THR001 — same generation of checks.