Skip to content

Investigate replacing exceptions with Go-style errors #2523

@SamWilsn

Description

@SamWilsn

Background

Exceptions are somewhat difficult to reason about. They are non-local control flow, and it isn't obvious looking at a function what exception types it can throw. They also make line-based code coverage tools less useful: you can't see if a particular error is triggered in a particular call site because there is no line representing the error case. For example:

def do_some_work(input):
    if input:
        raise Exception()

def opcode_foo():
    input = pop()
    do_some_work()

def opcode_bar():
    input = pop()
    do_some_work()

In the above, you can't tell if you've tested the error case in both opcode_foo and opcode_bar, only that at least one opcode has hit do_some_work's error condition. Contrast that with:

def do_some_work(input):
    if input:
        return Err()
    return Ok()

def opcode_foo():
    input = pop()
    result = do_some_work(input)
    if isinstance(result, Err):
        return result

def opcode_bar():
    input = pop()
    result = do_some_work(input)
    if isinstance(result, Err):
        return result

In this example, line-based code coverage can reveal whether the error case is tested for both opcodes.

Questions

  • How practical is Go-style error handling in Python?
  • Does it impact performance?
  • How messy does it make our code?
  • What insights do we get from the new line coverage information? Are we missing tests?

Implementation Notes

Here's one way to appease / make use of the typechecker:

O = TypeVar("O")
E = TypeVar("E")

@dataclass(frozen=True, slots=True)
class Ok(Generic[O]):
    value: O

    ok: ClassVar[Literal[True]] = True
    err: ClassVar[Literal[False]] = False

@dataclass(frozen=True, slots=True)
class Err(Generic[E]):
    error: E

    ok: ClassVar[Literal[False]] = False
    err: ClassVar[Literal[True]] = True


class ApplyBodyError:
    ...

def apply_body(
    block_env: BlockEnvironment,
    transactions: Tuple[Transaction, ...],
    ommers: Tuple[Header, ...],
) -> Ok[BlockOutput] | Err[ApplyBodyError]:
    return Ok(BlockOutput())


class FooError:
    ...

def foo() -> Ok[BlockOutput] | Err[ApplyBodyError] | Err[FooError]:
    result = apply_body(
        BlockEnvironment(), (Transaction(),), (Header(),)
    )
    if result.err:
        return result

    reveal_type(result.value) # Correctly narrowed to `BlockOutput`

    return result

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-spec-specsArea: Specification—The Ethereum specification itself (eg. `src/ethereum/*`)C-choreCategory: choreE-hardExperience: difficult, probably not for the faint of heart

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions