Mastering Solidity: Control Structures And Error Handling
Solidity, a statically-typed and contract-centric high-level language, is the cornerstone for crafting smart contracts on the Ethereum blockchain. Mastery of control structures and error handling is essential to develop top-tier smart contracts, ensuring robust and efficient decentralized applications.
In this comprehensive guide, we delve into the fundamental control structures of Solidity, including if
, else
, while
, do-while
, for
, break
, continue
, and return
.
Additionally, we’ll unravel the mechanisms of error handling in Solidity, covering revert
, assert
, require
, as well as the Error
and Panic
exceptions, complemented by the try-catch
block.
Through a series of practical demonstrations, you’ll gain hands-on experience to effectively navigate these concepts. Whether you’re just starting out or are an experienced Solidity developer, this article aims to enhance your understanding and refine your skills.
You can also watch the video here.
Table Of Contents
· Table Of Contents
· Control Structures
∘ If
∘ Else
∘ While
∘ How to set up your local blockchain to use console.log
∘ Do-While
∘ For
∘ Break
∘ Continue
∘ Return
∘ Hands on Demonstration
· Error handling: Assert, Require, Revert and Exceptions
∘ Assert statement and Panic Errors
∘ Require statement and Error
∘ Revert statement
· Try/Catch
· Further Exploration
· Conclusions
· References
Control Structures
Control structures in a programming language are constructs that manipulate the flow of execution. They allow the program to branch in different directions and loop through blocks of code based on certain conditions. Here are the main types of control structures:
- Sequential Control Structure: This is the default control structure where instructions are executed one after the other in the order they appear in the script.
- Selection or Conditional Control Structure: This allows one set of statements to be executed if a condition is true and another set of statements to be executed if the condition is false. Examples include
if
, andif-else
statements. - Iteration or Loop Control Structure: This allows a set of instructions to be repeatedly executed until a certain condition is met. Examples include
for
,while
, anddo-while
loops. - Jump Control Structure: This alters the execution flow by moving control to another part of the program. Examples include
break
,continue
, andreturn
.
These control structures are fundamental to programming and are found in most, if not all, programming languages including Solidity.
Solidity also supports exception handling in the form of try
/catch
statements, but only for external function calls and contract creation calls. Errors can be created using the revert statement.
If
The if
statement is used to specify a block of code to be executed if a specified condition is true. For example:
if (x > y) {
// code to be executed if x is greater than y
}
Else
The else
statement is used to specify a block of code to be executed if the same condition is false. For example:
if (x > y) {
// code to be executed if x is greater than y
} else {
// code to be executed if x is not greater than y
}
While
The while
statement is used to loop through a block of code as long as the specified condition is true. For example:
while (x < 10) {
// code to be executed while x is less than 10
x++;
}
The while
loop will executed the code inside the block {}
as long as the condition x < 10
is true. At the end of each iteration the variable x
is auto-incremented by 1 with the ++
operator by one. Once x
becomes greater than 10 the code after the while
block will be executed.
To make a practical test we could print the variable x
in the console:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "hardhat/console.sol";
contract MyContract {
function test() public view {
uint x = 0;
while (x < 5) {
console.log(x);
x++;
}
}
}
The previous code would output:
console.log:
0
1
2
3
4
How to set up your local blockchain to use console.log
Please note that in the previous contract we made use of the functionconsole.log
provided by the Hardhat contract console.sol
to print in the the console of the Hardhat local node. In fact as you can guess since in Ethereum the code is executed in a decentralized manner printing in the console is not as straightfoward as in other programming languages.
If you want to try this code you need to create a new Hardhat project with the following steps:
npm init -y
npm install --save-dev hardhat
npx hardhat init
Then:
- Choose Create an empty hardhat.config.js
- In the file hardhat.config.js change the Solidity version to: 0.8.25
Finally you need to start the Hardhat local blockchain with npx hardhat node
.
Now if you would like to use Remix to test this contract you should choose Dev-Hardhat Provider as environment in the Deploy & run transactions section, so that Remix can communicate with your local blockchain provided by Hardhat.
Do-While
The do
statement is used in conjunction with while
to create a do-while loop, which is a variant of the while loop. This loop will execute the block of code once, before checking if the condition is true, then it will repeat the loop as long as the condition is true. For example:
do {
// code to be executed
x++;
} while (x < 10);
The main difference between a while
loop and a do-while
loop is that the first could be never executed if the condition is not satisfied while the second is executed at least one time.
For
The for
statement is used to create a for loop, which consists of three parts: initialization, condition, and increment/decrement. For example:
for (uint i = 0; i < 10; i++) {
// code to be executed
}
- In the first expression we initilize the variable that will count the number of iteration
uint i = 0
- In the second expression we declare the condition that must hold true for the next iteration to be executed
i < 10
- In the third expression we express the update (increment or decrement) of the iteration variable
i
To make a pratical example we could print the variable i
in the console:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "hardhat/console.sol";
contract MyContract {
function test() public view {
for (uint i = 0; i < 5; i++) {
console.log(i);
}
}
}
The previous code would print:
console.log:
0
1
2
3
4
The for
loop is similar to the while
loop, and the following two examples are equivalent:
uint i= 0;
while (i < 10) {
// code to be executed while x is less than 10
i++;
}
// is equivalent to
for (uint i = 0; i < 10; i++) {
// code to be executed
}
When should we choose a for loop over a while loop?
- For loop: Suitable for a known number of iterations or when looping over ranges.
- While loop: Useful when the number of iterations is not known in advance or based on a condition.
Break
The break
statement is used to stop the execution of the loop immediately and transfer control to the next statement following the loop. For example:
for (uint i = 0; i < 10; i++) {
if (i == 5) {
break;
}
// code to be executed
}
Let’s make also a practical test by executing the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "hardhat/console.sol";
contract MyContract {
function test() public view {
for (uint i = 0; i < 10; i++) {
if (i == 5) {
break;
}
console.log(i);
}
}
}
The previous code will print:
console.log:
0
1
2
3
4
Note that the loop stopped at i == 5
because of the break
statement.
Continue
The continue
statement is used to skip the rest of the code inside the current iteration of the loop and continue with the next iteration. For example:
for (uint i = 0; i < 10; i++) {
if (i == 5) {
continue;
}
// code to be executed
}
Let’s make also a practical test by executing the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "hardhat/console.sol";
contract MyContract {
function test() public view {
for (uint i = 0; i < 10; i++) {
if (i == 5) {
continue;
}
console.log(i);
}
}
}
It will print:
0
1
2
3
4
6
7
8
9
You can see that for i
equal to 5 nothing was printed in the console because the continue
statement jumped to the next iteration.
Return
The return
statement is used to specify the value that a function should return. For example:
function add(uint x, uint y) public pure returns (uint) {
uint sum = x + y;
return sum;
}
Hands on Demonstration
The following code demonstrates all the statements that we have discussed before. For each function it has been created a corresponding function to test it. For instance: the isPositive
demonstrates theif-else
statement: if the input number x
is positive returnstrue
, otherwisefalse.
The function testIsPositive
is intended to test the function isPositive
by making different assetions, using the assert
statement: if the assertion is false the function will revert otherwise the function will be executed without errors.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
// Demonstrating the if-else statement
function isPositive(int x) public pure returns (bool) {
if (x > 0) {
return true;
} else {
return false;
}
}
// Demonstrating the while statement
function recursiveSum(uint x) public pure returns (uint) {
uint i = 0;
uint sum = 0;
while (i <= x) {
sum += i; // equivalent to sum = sum + i;
i++;
}
return sum;
}
// Demonstrating the do-while statement
function sumBackwards(uint start) public pure returns (uint) {
if (start == 0) {
return 0;
}
uint i = start;
uint sum = start;
do {
i--;
sum += i; // equivalent to sum = sum + i;
} while (i > 0);
return sum;
}
// Demonstrating the for statement
function factorialFuntion(uint x) public pure returns (uint) {
uint factorial = 1;
for (uint i = 1; i <= x; i++){
factorial *= i; // equivalent to factorial = factorial * i;
}
return factorial;
}
// Demonstrating the break statement
function stopCountAtFive(uint x) public pure returns (uint) {
uint i = 0;
while (i < x) {
if (i == 5) {
break;
}
i++;
}
return i;
}
// Demonstrating the continue statement
function skipSumAtThree(uint x) public pure returns (uint) {
uint i = 0;
uint sum = 0;
while (i < x) {
i++;
if (i == 3) {
continue;
}
sum += i; // equivalent to sum = sum + i;
}
return sum;
}
// Testing the isPositive function
function testIsPositive() public pure {
assert(isPositive(5) == true);
assert(isPositive(-5) == false);
}
// Testing the recursiveSum function
function testRecursiveSum() public pure {
assert(recursiveSum(0) == 0);
assert(recursiveSum(1) == 1);
assert(recursiveSum(2) == 3);
assert(recursiveSum(3) == 6);
assert(recursiveSum(4) == 10);
assert(recursiveSum(5) == 15);
}
// Testing the factorialFunction function
function testFactorialFunction() public pure {
assert(factorialFuntion(0) == 1);
assert(factorialFuntion(1) == 1);
assert(factorialFuntion(2) == 2);
assert(factorialFuntion(3) == 6);
assert(factorialFuntion(4) == 24);
assert(factorialFuntion(5) == 120);
}
// Testing the sumBackwards function
function testSumBackwards() public pure {
assert(sumBackwards(0) == 0);
assert(sumBackwards(1) == 1);
assert(sumBackwards(2) == 3);
assert(sumBackwards(3) == 6);
assert(sumBackwards(4) == 10);
assert(sumBackwards(5) == 15);
}
// Testing the stopCountAtFive function
function testStopCountAtFive() public pure {
assert(stopCountAtFive(0) == 0);
assert(stopCountAtFive(1) == 1);
assert(stopCountAtFive(2) == 2);
assert(stopCountAtFive(3) == 3);
assert(stopCountAtFive(4) == 4);
assert(stopCountAtFive(5) == 5);
assert(stopCountAtFive(6) == 5);
}
// Testing the skipSumAtThree function
function testSkipSumAtThree() public pure {
assert(skipSumAtThree(0) == 0);
assert(skipSumAtThree(1) == 1);
assert(skipSumAtThree(2) == 3);
assert(skipSumAtThree(3) == 3);
assert(skipSumAtThree(4) == 7);
assert(skipSumAtThree(5) == 12);
}
}
Error handling: Assert, Require, Revert and Exceptions
In Solidity, exceptions are used for error handling. They can occur at both compile time and runtime.
When the Solidity code is compiled to byte code, a syntax error check happens at compile-time. If there are any errors, the compiler will not compile the code until the errors are resolved.
Runtime errors are more challenging to catch and mainly occur while executing the contracts. Some of the runtime errors include out-of-gas error, data type overflow error, divide by zero error, array-out-of-index error, etc.
Exceptions that occur in a sub-call will automatically propagate upwards (meaning, they are rethrown) unless they are intercepted by a try/catch statement. However, there are exceptions to this rule. The send
function and the low-level functions call
, delegatecall
, and staticcall
behave differently: instead of propagating the exception upwards, they return false
as their first return value in case of an exception.
Exceptions have the ability to carry error information, which is returned to the caller as instances of errors. The built-in errors, namely Error(string)
and Panic(uint256)
, are utilized by specific functions: assert
, revert
and require
. The Error
is employed for standard error situations, while Panic
is used for errors that should not occur in code that is free of bugs.
Assert statement and Panic Errors
The assert
function is utilized to verify conditions and it will trigger an exception if the specified condition is not fulfilled. This function generates an error of the Panic(uint256)
type.
The assert
function should be exclusively used for internal error testing and invariant checking. Code that is functioning correctly should never produce a Panic
, even in response to incorrect external input. If a Panic
is generated, it indicates a bug in your contract that needs to be addressed.
When a revert
is triggered all changes made to the EVM state are reverted.
A Panic exception is generated in the following situations. The error code supplied with the error data indicates the kind of panic:
- 0x00: Used for generic compiler inserted panics.
- 0x01: If you call
assert
with an argument that evaluates to false. - 0x11: If an arithmetic operation results in underflow or overflow outside of an
unchecked { ... }
block. - 0x12; If you divide or modulo by zero (e.g.
5 / 0
or23 % 0
). - 0x21: If you convert a value that is too big or negative into an enum type.
- 0x22: If you access a storage byte array that is incorrectly encoded.
- 0x31: If you call
.pop()
on an empty array. - 0x32: If you access an array,
bytesN
or an array slice at an out-of-bounds or negative index (i.e.x[i]
wherei >= x.length
ori < 0
). - 0x41: If you allocate too much memory or create an array that is too large.
- 0x51: If you call a zero-initialized variable of internal function type.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
function exampleFunction(uint256 x) public pure returns (uint) {
assert(x > 0);
return x;
}
function divide(uint x, uint y) public pure returns (uint) {
return x / y;
}
}
If we call the exampleFunction
with x equal to 0 on Remix, we will get this output:
call to MyContract.exampleFunction
call to MyContract.exampleFunction errored: Error occurred: execution reverted: assert(false).
execution reverted: assert(false)
If we call the divide function with x = 10 and y = 0 on Remix we will get this output:
call to MyContract.divide
call to MyContract.divide errored: Error occurred: execution reverted: division or modulo by zero.
execution reverted: division or modulo by zero
In either cases the execution of the function has been reverted and a Panic Error has been thrown that has caused the Remix console to print the previous logs.
To demonstrate that Solidity will throw a Panic
error with a specific error code under certain circumstances, we can make use of the try-catch
block, which will be explained later:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
uint[] myArray;
function exampleFunction(uint256 x) external pure returns (uint) {
assert(x > 0);
return x;
}
function popArray() external {
myArray.pop();
}
function divide(uint x, uint y) public pure returns (uint) {
return x / y;
}
// Demonstrating that a false assertion throws a Panic error of code 0x01
function testExampleFunction() public view {
try this.exampleFunction(0) {
} catch Panic(uint errorCode) {
assert(errorCode == 0x01);
return;
}
assert(false);
}
// Demonstrating that calling .pop on an empty array throws a Panic error of code 0x31
function testPopArray() public {
try this.popArray() {
} catch Panic(uint errorCode) {
assert(errorCode == 0x31);
return;
}
assert(false);
}
// Demonstrating that dividing by zero throws a Panic error of code 0x12
function testDivide() public view {
try this.divide(10, 0) {
} catch Panic(uint errorCode) {
assert(errorCode == 0x12);
return;
}
assert(false);
}
}
In this code we have defined the exampleFunction
that raises a Panic
error if it is called with an input greater than zero: assert(x > 0)
. In the test function testExampleFunction
we are calling the exampleFunction
with 0 as input, and the catch
statement is execute only if the exampleFunction
raises a Panic
error. Inside the test we are testing that the errorCode
of the Panic
error is equal to 0x01
with assert(errorCode == 0x01)
then we return. If the function should not raise a Panic
error, the test would fail because it would be executed assert(false)
outside the catch block.
In the function testPopArray
we are demonstrating that calling .pop
on an empty array raises a Panic
error of code 0x31
. A similar test is performed in the functoin testDivide
where we are testing that dividing a number by zero raises a Panic
error with code 0x12
.
Require statement and Error
The require
function is used for input validation. It checks whether a certain condition is true, and if it’s not, it reverts all changes made to the state during the current call (like when an exception is thrown in other programming languages) and also provides an error message.
The require
function provides three overloads:
require(bool)
which will revert without any data (not even an error selector).require(bool, string)
which will revert with anError(string)
.require(bool, error)
which will revert with the custom, user supplied error provided as the second argument.
An Error(string)
exception (or an exception without data) is generated by the compiler in the following situations:
- Calling
require(x)
wherex
evaluates tofalse
. - If you use
revert()
orrevert("description")
. - If you perform an external function call targeting a contract that contains no code.
- If your contract receives Ether via a public function without
payable
modifier (including the constructor and the fallback function). - If your contract receives Ether via a public getter function.
For the following cases, the error data from the external call (if provided) is forwarded. This means that it can either cause an Error
or a Panic
(or whatever else was given):
- If a
.transfer()
fails. - If you call a function via a message call but it does not finish properly (i.e., it runs out of gas, has no matching function, or throws an exception itself), except when a low level operation
call
,send
,delegatecall
,callcode
orstaticcall
is used. The low level operations never throw exceptions but indicate failures by returningfalse
. - If you create a contract using the
new
keyword but the contract creation does not finish properly.
You can optionally provide a message string or a custom error to require
, but not to assert
. If you do not provide a string or custom error argument to require
, it will revert with empty error data, not even including the error selector.
However, in our code, it is advisable to throw errors that contain a meaningful message. This can help us understand why the execution was interrupted.
Similar to the assert
function, when a revert
is triggered internally in Solidity, all changes made to the EVM state are reverted.
Additionally we can catch the Errors thrown by a revert statement by means of a try
/catch
block.
To demonstrate the require
function we can use the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
function divide(uint x, uint y) public pure returns (uint) {
require(y > 0, "The denominator must be greater than zero");
return x / y;
}
}
This time we have put at the beginning of the function divide
a require
statement to check the input of our function. In particular if we pass 0 as y the require statement will raise an exception and the function will stop its execution.
For instance if we call the divide function with x = 10 and y = 0 we will see this log in the Remix console:
call to MyContract.divide
call to MyContract.divide errored: Error occurred: execution reverted: The denominator must be greater than zero.
execution reverted: The denominator must be greater than zero
To prove that the revert statment is throwing an error of type Error
and not for example a Panic
we can make use of a try-catch block:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
function divide(uint x, uint y) public pure returns (uint) {
require(y > 0, "The denominator must be greater than zero");
return x / y;
}
// Demonstrating that divide throws an Error
function testDivide() public view {
try this.divide(10, 0) {
} catch Error(string memory message) {
assert(compareStrings(message, "The denominator must be greater than zero"));
return;
}
assert(false);
}
// Compares two strings and returns true if they are equal
function compareStrings(string memory str1, string memory str2) public pure returns (bool) {
return keccak256(abi.encodePacked(str1)) == keccak256(abi.encodePacked(str2));
}
}
In the catch
block we are catching an error of type Error
and we are testing that the message that the Error
carries is exactly ‘The denominator must be greater than zero’.
Revert statement
To force a direct revert during a function execution we can use the revert
statement. It takes a custom error as direct argument without parentheses:
revert CustomError(arg1, arg2);
For backward-compatibility reasons, there is also the revert()
function, which uses parentheses and accepts a string:
revert(); revert(“description”);
The error data will be passed back to the caller and can be caught there. Using revert()
causes a revert without any error data while revert("description")
will create an Error(string)
error.
Using a custom error instance will usually be much cheaper than a string description, because you can use the name of the error to describe it, which is encoded in only four bytes.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
error NumberNotValid();
function divide(uint x, uint y) public pure returns (uint) {
if (y == 0) {
revert NumberNotValid();
}
return x / y;
}
}
The previous code demonstrates how the revert
statement throws a custom error: if y equals 0 it will be thrown a NumberNotValidError
error.
In fact if we call the divide
function with argument y = 10 and x = 0, the Remix console will output:
call to MyContract.divide errored: Error occurred: execution reverted.
execution reverted
The transaction has been reverted to the initial state.
Error provided by the contract:
NumberNotValid
Parameters:
{}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
function divide(uint x, uint y) public pure returns (uint) {
if (y == 0) {
revert("The denominator must be greater than zero");
}
return x / y;
}
// Demonstrating that divide throws an Error
function testDivide() public view {
try this.divide(10, 0) {
} catch Error(string memory message) {
assert(compareStrings(message, "The denominator must be greater than zero"));
return;
}
assert(false);
}
// Compares two strings and returns true if they are equal
function compareStrings(string memory str1, string memory str2) public pure returns (bool) {
return keccak256(abi.encodePacked(str1)) == keccak256(abi.encodePacked(str2));
}
}
In this second example the revert
function will throw an error of type Error.
As the testDivide
function demonstrates the revert
function is equivalent to the require
statement we have used earlier:
require(y > 0, "The denominator must be greater than zero");
// is equivalent to:
if (y == 0) {
revert("The denominator must be greater than zero");
}
However, we can catch errors of type Error
or Panic
in a try
block, but we cannot specifically catch custom errors.
Try/Catch
As we have seen in the previous example a failure in an external call can be caught using a try/catch statement:
try someExternalContract.someFunction() {
// some code here
} catch Error(string memory reason) {
// This block will catch an Error thrown by the external call
} catch Panic(uint errorCode) {
// This block will catch a Panic error thrown by the external call
} catch (bytes memory lowLevelData) {
// is executed if the error signature do not match the other clauses and provides low-level error data
} catch {
// is executed if the error signature do not match the other clauses but no low-level error data is provided
}
The try
keyword has to be followed by an expression representing an external function call or a contract creation (new ContractName()
). Errors inside the expression are not caught (for example if it is a complex expression that also involves internal function calls), only a revert happening inside the external call itself. The returns
part (which is optional) that follows declares return variables matching the types returned by the external call. In case there was no error, these variables are assigned and the contract’s execution continues inside the first success block. If the end of the success block is reached, execution continues after the catch
blocks.
Solidity supports different kinds of catch blocks depending on the type of error:
catch Error(string memory reason) { ... }
: This catch clause is executed if the error was caused byrevert("reasonString")
orrequire(false, "reasonString")
(or an internal error that causes such an exception).catch Panic(uint errorCode) { ... }
: If the error was caused by a panic, i.e. by a failingassert
, division by zero, invalid array access, arithmetic overflow and others, this catch clause will be run.catch (bytes memory lowLevelData) { ... }
: This clause is executed if the error signature does not match any other clause, if there was an error while decoding the error message, or if no error data was provided with the exception. The declared variable provides access to the low-level error data in that case.catch { ... }
: If you are not interested in the error data, you can just usecatch { ... }
(even as the only catch clause) instead of the previous clause.
In order to catch all error cases, you have to have at least the clause catch { ...}
or the clause catch (bytes memory lowLevelData) { ... }
.
In the following example we demonstrate how it is possible to catch a custom error with the clause catch (bytes memory lowLevelData)
. In fact the catch clauses with Panic
or Error
do not catch the error since they have the assert(false)
statement inside and if they would catch the error the test would fail. Instead the error is only catched by the the last clause catch (bytes memory lowLevelData)
and we can also retrieve the bytes associeted to the error.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
error NumberNotValid();
function divide(uint x, uint y) public pure returns (uint) {
if (y == 0) {
revert NumberNotValid();
}
return x / y;
}
// demonstrates how to catch a custom error with catch (bytes memory lowLevelData)
function testDivide() public view {
try this.divide(10, 0) {
} catch Error(string memory message) {
assert(false);
return;
} catch Panic(uint errorCode) {
assert(false);
return;
} catch (bytes memory lowLevelData) {
assert(lowLevelData.length != 0);
return;
}
assert(false);
}
}
In the following example we demonstrate how to catch a custom error with a simple catch
clause.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract MyContract {
error NumberNotValid();
function divide(uint x, uint y) public pure returns (uint) {
if (y == 0) {
revert NumberNotValid();
}
return x / y;
}
// demonstrates how to catch a custom error with catch (bytes memory lowLevelData)
function testDivide() public view {
try this.divide(10, 0) {
} catch Error(string memory message) {
assert(false);
return;
} catch Panic(uint errorCode) {
assert(false);
return;
} catch {
assert(true);
return;
}
assert(false);
}
}
Further Exploration
For those eager to dive into coding Solidity smart contracts, I recommend exploring the following resources:
- Ethereum Blockchain And Smart Contracts 101
- Mastering Solidity: A Comprehensive Guide to Data Types
- Mastering Solidity: A Comprehensive Guide to Contracts
- How to Develop Smart Contracts with Hardhat on Ethereum.
- How to Create Your Own Cryptocurrency with OpenZeppelin’s ERC-20 Contracts.
- How to Create ERC-721 NFTs on Ethereum with OpenZeppelin: A Step-by-Step Tutorial.
For those who want to understand more about how the blockchain works I can recommend the following article:
Conclusions
I trust that this article has served as a valuable resource in your exploration of Solidity’s control structures and error handling. Should you have any inquiries or come across any discrepancies, I encourage you to share your thoughts in the comments section. Here’s to your coding journey, may it be filled with discovery and success!