nomoixyz / vulcan

Development framework for Foundry projects
https://nomoixyz.github.io/vulcan/
MIT License
286 stars 18 forks source link

Result/Error improvements #187

Closed vdrg closed 1 year ago

vdrg commented 1 year ago

We've been discussing a few ideas for improving the way Errors are structured within our new "Rust-like" results implementation. Some ideas:

1. Current way of handling errors

Base Error: struct Error { id, message, data? }

For each module, we define:

library LibFooError {
    bytes32 constant ERROR1_ID = ...;
    bytes32 constant ERROR2_ID = ...;

    function error1(...args) internal pure returns(FooResult memory) { }
    function error2(...args) internal pure returns(FooResult memory) { }
}

Usage:

// To return the error
return LibFooError.error1(...args);

// To handle the error
if (res.isError(LibFooError.ERROR1_ID)) {
    (...decodedArgs) = abi.decode(res.toError().data, (...argTypes));
}

Pros:

Cons:

2. Errors as micro libraries

Base Error: struct Error { id, message, data? }

For each specific error, we define:

library FooError {
    bytes32 constant id = ...;
    function create(...args) internal pure returns(Result memory) { } // Alternatively this could return the specific result we want
    function decode(Error memory) internal pure returns(...decodedArgs) { } // Only if Error encodes data
}

Usage:

    library someModule {
        function returnErrors() internal returns (ModuleResult memory) {
            return ModuleResult(FooError.create(bytes32("Hello, World!")));
        }
    }

    function foo() external {
        ModuleResult memory result0 = someModule.returnErrors();

        if (result0.isOk()) {
            // Do something
        }

        if (result0.isError(FooError.id) {
             bytes32 payload = FooError.decode(result0.toError());

             expect(payload).toEqual(bytes32("Hello, World!")); // true
        }
    }

Pros:

Cons:

3. Using custom errors

Base Error: struct Error { message, data }

We modify the Error struct functions and we add

    function extract(Error memory self) internal pure returns (bytes4 selector, bytes memory payload) {
        (selector, payload) = extractSelector(self.data);
    }

Our module specific error library will look something like

library ModuleErrors {
    error Error0(bytes32);

    // `data` is the additional information we want to store on the error
    function error0(bytes32 data) internal pure returns (TestResult memory) {
        return TestResult(Error("error0", abi.encodeWithSelector(Error0.selector, data)).toResult());
    }
}

Usage:

    library someModule {
        function returnErrors() internal returns (ModuleResult memory) {
            return ModuleErrors.error0(bytes32("Hello, World!"));
        }
    }

    function foo() external {
        TestResult memory result0 = someModule.returnErrors();

        if (result0.isError()) {
            Error memory err = result0.toError();

            (bytes4 selector, bytes memory payload) = err.extract();

            if (selector == TestErrors.Error0.selector) {
                bytes32 payload = abi.decode(payload, (bytes32));

                expect(payload).toEqual(bytes32("Hello, World!")); // true
            }
        }
    }

Pros:

Cons:

4. Errors as functions

Base Error: type Error is bytes32

For each specific error we define a function:

    using LibError for *;

    function FooError(bytes32 value) internal pure returns (Error) {
        return FooError.encodeError("Foo error message", value);
    }

Usage:

    library someModule {
        function returnErrors() internal returns (ModuleResult memory) {
            return ModuleResult(FooError(bytes32("Hello, World!")).toResult());
        }
    }

    function foo() external {
        ModuleResult memory result0 = someModule.returnErrors();

        if (result0.isOk()) {
            // Do something
        }

        Error err = result0.toError();
        if (err.matches(FooError) {
             bytes32 payload = err.decodeAs(FooError);

             expect(payload).toEqual(bytes32("Hello, World!")); // true
        }
    }

Pros:

Cons:

vdrg commented 1 year ago

We decided to go for 4. implemented in #189