ethereum / ethereum/execution-specs

Investigate replacing exceptions with Go-style errors

Open
#2,523 2 comments 0 reactions 0 assignees View on GitHub
A-spec-specs C-chore E-hard stale
Dominant language
Python
Stars
1.2k
Forks
505
Avg merge
2d 14h
Merged PRs (30d)
116

Description

# 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:

```python
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:

```python
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:

```python
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
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.