I’ve often seen it mentioned online that one of the downsides of the Go programming language is its bloated error handling.
something, err := function1()
if err != nil {
return err
}
Go deliberately avoids exceptions and favors explicit handling with (value, error)
returns to keep control flow visible and predictable. This decision aligns with
Go’s core values: simplicity, readability, and no hidden control flow.
But is this really bloated, or just clear and intentional? In this post, I’ll compare Go’s error handling to TypeScript’s, and argue that Go’s way — while verbose — can actually lead to cleaner, more robust code in practice.
See the code in Go
The following Go code will be re-implemented in TypeScript. It is really just a simple dummy code: it contains two function that may return an error. In the main function, I call them and handle the errors using what some call “bloated” error handling.
package main
import (
"errors"
"fmt"
"math/rand"
"os"
)
func function1() (int, error) {
number := rand.Intn(100)
if number < 20 {
return 0, errors.New("you have been unlucky")
}
return number, nil
}
func function2() (int, error) {
number := rand.Intn(100)
if number > 80 {
return 0, errors.New("you have been unlucky")
}
return number, nil
}
func main() {
num, err := function1()
if err != nil {
fmt.Println("Error is", err.Error())
os.Exit(1)
}
fmt.Println("First number is", num)
num, err = function2()
if err != nil {
fmt.Println("Error is", err.Error())
os.Exit(1)
}
fmt.Println("Second number is", num)
}
Now, try to implement in TS
First, naive implementation
The first try might look like this:
function function1(): number {
const number = Math.floor(Math.random() * 100);
if (number < 20) {
throw new Error("you have been unlucky");
}
return number;
}
function function2(): number {
const number = Math.floor(Math.random() * 100);
if (number > 80) {
throw new Error("you have been unlucky");
}
return number;
}
function main() {
const num1 = function1();
console.log("First number is", num1);
const num2 = function2();
console.log("Second number is", num2);
}
main();
Catch the errors
But it has serious flaws from multiple perspectives. One issue is that each function can throw an error, but these errors are not caught. To fix that, let’s evolve the TypeScript code.
function function1(): number {
const number = Math.floor(Math.random() * 100);
if (number < 20) {
throw new Error("you have been unlucky");
}
return number;
}
function function2(): number {
const number = Math.floor(Math.random() * 100);
if (number > 80) {
throw new Error("you have been unlucky");
}
return number;
}
function main() {
try {
const num1 = function1();
console.log("First number is", num1);
const num2 = function2();
console.log("Second number is", num2);
} catch (err) {
if (err instanceof Error) {
console.error(err.message);
}
process.exit(1);
}
}
main();
Final version
You might think: “This looks identical to Go — it doesn’t have those bloated error blocks, just one elegant try-catch.” But you are still wrong. Why? Even if you catch the error, you don’t know where the error was coming! So, we should implement a try-catch for each function. The main function is now this.
function main() {
try {
const num1 = function1();
console.log("First number is", num1);
} catch (err) {
if (err instanceof Error) {
console.error(err.message);
}
process.exit(1);
}
try {
const num1 = function1();
console.log("First number is", num1);
} catch (err) {
if (err instanceof Error) {
console.error(err.message);
}
process.exit(1);
}
}
But that assumption is still incorrect. Why? Because nothing in the TS code indicates that a function may return an error! To be honest, this is one of my biggest issue with JS/TS, I cannot see what can return with error and where. Meanwhile, Go (or Rust) error handling may look bloated, but it can tell you that “Look, here you can have this and that problem. Handle them if you want.” This gives some robustness to my code. Of course, I can ignore the error in my Go code as well, but at least the error is not just a “polite comment” — it’s part of the type system.
Can TS code work like Go code?
Let’s try to implement something in TS that clearly indicates when a function returns an error!
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
function function1(): Result<number> {
const number = Math.floor(Math.random() * 100);
if (number < 20) {
return { ok: false, error: new Error("you have been unlucky") };
}
return { ok: true, value: number };
}
function function2(): Result<number> {
const number = Math.floor(Math.random() * 100);
if (number > 80) {
return { ok: false, error: new Error("you have been unlucky") };
}
return { ok: true, value: number };
}
function main() {
const result1 = function1();
if (result1.ok === false) {
console.error("function1 failed:", result1.error.message);
process.exit(1);
}
console.log("First number is", result1.value);
const result2 = function2();
if (result2.ok === false) {
console.error("function2 failed:", result2.error.message);
process.exit(1);
}
console.log("Second number is", result2.value);
}
main();
Unfortunately, even if the Result above would be implemented in personal
projects, it’s still not enough, since the TypeScript system itself does not
implement it, so the libraries does not follow this way.
Final words
When I started programming a long time ago, I began with C and I like knowing the type of something – what I can expect from it.. Later I tried other languages like C#, TS, Rust, Go. I admit at first glance, the way Rust and Go handle errors may look bloated, but the fact that I DO KNOW where potential errors can occur gives my code a lot more stability
For comparison, this is the main function in Go and TS. To me, the Go version looks smoother and easier to read.
func main() {
num, err := function1()
if err != nil {
fmt.Println("Error is", err.Error())
os.Exit(1)
}
fmt.Println("First number is", num)
num, err = function2()
if err != nil {
fmt.Println("Error is", err.Error())
os.Exit(1)
}
fmt.Println("Second number is", num)
}
function main() {
try {
const num1 = function1();
console.log("First number is", num1);
} catch (err) {
if (err instanceof Error) {
console.error(err.message);
}
process.exit(1);
}
try {
const num1 = function1();
console.log("First number is", num1);
} catch (err) {
if (err instanceof Error) {
console.error(err.message);
}
process.exit(1);
}
}
Tip (Optional): Create a Go Error Handling Snippet for LazyVim
Same snippet works in VS Code, but I use Neovim (with LazyVim distribution). To make things, easier, I like to define snippets, for tasks like Go error handling too.
To make it work with LazyVim, create following two files.
Content of ~/.config/nvim/snippets/package.json:
{
"name": "personal-snippets",
"contributes": {
"snippets": [
{
"language": "go",
"path": "./go.json"
}
]
}
}
Then create snippet for Go language. If you just use this one snippet, then file is the following.
{
"Error handling": {
"prefix": "iferr",
"description": "Insert Go error handler",
"body": ["if err != nil {", " return $1 err", "}"]
}
}
What does it do? When you type iferr and press Tab or Enter, it pastes this
snippet, and places the cursor, in edit mode, between ‘return’ and ’err’
words. So easy and quick to type additional values if return value is a tuple.
🎬 Play me!
