ethereum / ethereum/execution-specs
Investigate replacing exceptions with Go-style errors
- 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
Assessment
This issue has not been assessed yet.