Skip to content
Sarj Code Standards

no-discarded-tagged-outcome

Handle a returned tagged outcome instead of discarding it.

Why

A tagged return union makes possible outcomes visible in the type contract. Discarding the value bypasses the caller's opportunity to handle its alternatives, including a declared failure.

Fix

Use or forward the returned outcome. Handle its variants with match and a typed exhaustive fallback such as assert_never; preserve payloads and side effects. Do not delete the operation or hide the result behind a dummy assignment.

Examples

Before — flagged A stable alias still returns an outcome that needs handling
app/outcomes.py
from typing import Annotated, Literal
from pydantic import BaseModel, Field
class Success[T](BaseModel):
kind: Literal["success"] = "success"
value: T
class Error[E](BaseModel):
kind: Literal["error"] = "error"
error: E
class Denied(BaseModel):
code: Literal["denied"] = "denied"
name: str
type Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator="kind")]
def save(name: str, allowed: bool) -> Result[str, Denied]:
return (
Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))
)
persist = save
persist("record-7", False)
After — preferred The alias result is propagated to its caller
app/outcomes.py
from typing import Annotated, Literal
from pydantic import BaseModel, Field
class Success[T](BaseModel):
kind: Literal["success"] = "success"
value: T
class Error[E](BaseModel):
kind: Literal["error"] = "error"
error: E
class Denied(BaseModel):
code: Literal["denied"] = "denied"
name: str
type Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator="kind")]
def save(name: str, allowed: bool) -> Result[str, Denied]:
return (
Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))
)
persist = save
def run() -> Result[str, Denied]:
return persist("record-7", False)
Before — flagged A returned generic success/error contract is discarded
app/outcomes.py
from typing import Annotated, Literal
from pydantic import BaseModel, Field
class Success[T](BaseModel):
kind: Literal["success"] = "success"
value: T
class Error[E](BaseModel):
kind: Literal["error"] = "error"
error: E
class Denied(BaseModel):
code: Literal["denied"] = "denied"
name: str
type Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator="kind")]
def save(name: str, allowed: bool) -> Result[str, Denied]:
return (
Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))
)
save("record-7", False)
After — preferred The caller propagates the complete generic result
app/outcomes.py
from typing import Annotated, Literal
from pydantic import BaseModel, Field
class Success[T](BaseModel):
kind: Literal["success"] = "success"
value: T
class Error[E](BaseModel):
kind: Literal["error"] = "error"
error: E
class Denied(BaseModel):
code: Literal["denied"] = "denied"
name: str
type Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator="kind")]
def save(name: str, allowed: bool) -> Result[str, Denied]:
return (
Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))
)
def run(name: str, allowed: bool) -> Result[str, Denied]:
return save(name, allowed)
Before — flagged A tagged outcome is discarded
app/outcomes.py
from typing import Literal
from pydantic import BaseModel
class Accepted(BaseModel):
kind: Literal["accepted"]
value: str
class Rejected(BaseModel):
kind: Literal["rejected"]
reason: str
type Outcome = Accepted | Rejected
def check(value: str) -> Outcome:
return Accepted(kind="accepted", value=value)
check("value")
After — preferred The caller forwards the outcome
app/outcomes.py
from typing import Literal
from pydantic import BaseModel
class Accepted(BaseModel):
kind: Literal["accepted"]
value: str
class Rejected(BaseModel):
kind: Literal["rejected"]
reason: str
type Outcome = Accepted | Rejected
def check(value: str) -> Outcome:
return Accepted(kind="accepted", value=value)
def run() -> Outcome:
return check("value")