<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Honeybadger Developer Blog</title>
  <subtitle>Useful articles for web developers in Ruby, Javascript, Elixir, and more</subtitle>
  <id>https://www.honeybadger.io/blog/</id>
  <link href="https://www.honeybadger.io/blog/"/>
  <link href="https://www.honeybadger.io/blog/feed.xml" rel="self"/>
  <updated>2026-08-13T07:00:00+00:00</updated>
  <author>
    <name>The Honeybadger.io Crew</name>
  </author>
  <entry>
    <title>A comprehensive guide to Fly.io logging</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/fly-io-logging/"/>
    <id>https://www.honeybadger.io/blog/fly-io-logging/</id>
    <published>2026-08-13T07:00:00+00:00</published>
    <updated>2026-08-13T07:00:00+00:00</updated>
    <author>
      <name>Muhammed Ali</name>
    </author>
    <summary type="text">Logging is an import part of debugging an app. Without a good method of catching errors or logs in general, you would end up with uncaught issues and could also lose valuable customers in the process. Read this article to learn how to effectively handle logs on Fly.io.</summary>
    <content type="html">&lt;p&gt;Deployment is not the end of shipping your application. From time to time, you will get errors that you will need to attend to. Without a good method of catching errors or logs in general, you could end up with uncaught issues that might cost you valuable customers in the process.&lt;/p&gt;
&lt;p&gt;In this article, you will learn how to catch logs for an application deployed on Fly.io. You will learn how Fly.io logging works, then learn ways to handle logs natively on the platform. Finally, we will see a better way of logging with Honeybadger.&lt;/p&gt;
&lt;h2&gt;How does Fly.io logging work?&lt;/h2&gt;
&lt;p&gt;Fly.io runs deployed apps inside a lightweight VM booted from an unpacked image. In each container where the application is running, a process (&lt;code&gt;init&lt;/code&gt;) is activated to run and monitor your app. This &lt;code&gt;init&lt;/code&gt; program, along with others, collects the application&apos;s output from &lt;code&gt;stdout&lt;/code&gt; or &lt;code&gt;stderr&lt;/code&gt; and redirects it to the host machine.&lt;/p&gt;
&lt;p&gt;It is not enough to just collect logs; we still need a way to handle and manage any potential errors or logs. In the host machine, Fly.io uses a socket to &lt;a href=&quot;https://fly.io/docs/monitoring/logging-overview/&quot;&gt;send the logs to&#xa0;Vector&lt;/a&gt;. The logs are then sent into Fly&apos;s internal NATS cluster, where Clients can subscribe to specific topics. In Fly log shipper, Vector acts as a NATS client, reads the logs, and ships them to a Vector Sink (e.g., Honeybadger).&lt;/p&gt;
&lt;h2&gt;Basic way to access logs on Fly.io&lt;/h2&gt;
&lt;p&gt;Fly.io makes it easy to access application logs. Since applications running on Fly.io write output to standard output (&lt;code&gt;stdout&lt;/code&gt;) and standard error (&lt;code&gt;stderr&lt;/code&gt;), Fly.io automatically collects and streams those logs for you.&lt;/p&gt;
&lt;p&gt;In this section, you will deploy a sample FastAPI project to Fly.io. The application is intentionally built to generate application logs for the learning process.&lt;/p&gt;
&lt;h3&gt;Building a FastAPI application that generates logs&lt;/h3&gt;
&lt;p&gt;To demonstrate logging, we will build a simple API with endpoints that generate different types of log messages.&lt;/p&gt;
&lt;p&gt;Create a project directory and move into it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir flyio-logging-demo
cd flyio-logging-demo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a virtual environment and activate it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m venv venv
source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install FastAPI and Uvicorn:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install fastapi uvicorn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a file named &lt;code&gt;main.py&lt;/code&gt; and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import logging
from fastapi import FastAPI

app = FastAPI()

logging.basicConfig(
    level=logging.INFO,
    format=&amp;quot;%(asctime)s %(levelname)s %(message)s&amp;quot;
)

logger = logging.getLogger(__name__)

@app.get(&amp;quot;/&amp;quot;)
def home():
    logger.info(&amp;quot;User created&amp;quot;)
    return {&amp;quot;message&amp;quot;: &amp;quot;Hello from Fly.io&amp;quot;}

@app.get(&amp;quot;/login&amp;quot;)
def login():
    logger.warning(&amp;quot;Rate limit approaching&amp;quot;)
    return {&amp;quot;message&amp;quot;: &amp;quot;Login request received&amp;quot;}

@app.get(&amp;quot;/error&amp;quot;)
def error():
    logger.error(&amp;quot;Database connection failed&amp;quot;)
    return {&amp;quot;message&amp;quot;: &amp;quot;Error logged&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application contains three endpoints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; generates an informational log&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/login&lt;/code&gt; generates a warning log&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/error&lt;/code&gt; generates an error log&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fly.io automatically captures the following logs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(&amp;quot;Application started&amp;quot;)
logger.info(&amp;quot;User created&amp;quot;)
logger.warning(&amp;quot;Rate limit approaching&amp;quot;)
logger.error(&amp;quot;Database connection failed&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As long as your application writes output to standard output or standard error, Fly.io will collect and surface those logs.&lt;/p&gt;
&lt;p&gt;Now you can run your app with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;uvicorn main:app --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open another terminal and send requests to generate logs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:8000/ -w &amp;quot;\n&amp;quot; &amp;amp;&amp;amp; curl http://localhost:8000/login -w &amp;quot;\n&amp;quot; &amp;amp;&amp;amp; curl http://localhost:8000/error -w &amp;quot;\n&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your terminal should display output similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;{&amp;quot;message&amp;quot;:&amp;quot;Hello from Fly.io&amp;quot;}
{&amp;quot;message&amp;quot;:&amp;quot;Login request received&amp;quot;}
{&amp;quot;message&amp;quot;:&amp;quot;Error logged&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that the application is generating logs, we can deploy it to Fly.io.&lt;/p&gt;
&lt;h3&gt;Containerizing the application&lt;/h3&gt;
&lt;p&gt;Here we will put the application in a Docker container for easy deployment. Start by creating a file named &lt;code&gt;requirements.txt&lt;/code&gt; and adding the following to it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;fastapi
uvicorn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create a file named &lt;code&gt;Dockerfile&lt;/code&gt; and copy and paste this into it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-docker&quot;&gt;FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD [&amp;quot;uvicorn&amp;quot;, &amp;quot;main:app&amp;quot;, &amp;quot;--host&amp;quot;, &amp;quot;0.0.0.0&amp;quot;, &amp;quot;--port&amp;quot;, &amp;quot;8080&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Dockerfile installs the application dependencies and starts the FastAPI server.&lt;/p&gt;
&lt;h3&gt;Creating a Fly.io application&lt;/h3&gt;
&lt;p&gt;Assuming you already have an account on Fly.io, &lt;a href=&quot;https://fly.io/docs/flyctl/install/&quot;&gt;install Fly.io CLI&lt;/a&gt; and log in to Fly.io using the CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly auth login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initialize a new Fly application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly launch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fly.io will ask a few questions about your application configuration and then generate a &lt;code&gt;fly.toml&lt;/code&gt; file. You can review the generated configuration and accept the defaults for this tutorial.&lt;/p&gt;
&lt;p&gt;Now you can deploy your application with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fly.io will build the container image and start a Machine running your FastAPI application.
After deployment completes, open the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly open
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see the JSON response:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;message&amp;quot;: &amp;quot;Hello from Fly.io&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now generate some logs from the deployed application by sending requests to each endpoint or opening them in the browser.&lt;/p&gt;
&lt;p&gt;Each request produces log output that Fly.io collects automatically. You can tail live logs with the &lt;code&gt;fly logs&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see output similar to this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;2026-05-31T17:14:42Z app[d89492dc3e5208] cdg [info]2026-05-31 17:14:42,377 INFO User created
2026-05-31T17:14:42Z app[d89492dc3e5208] cdg [info]INFO:     172.16.45.10:59882 - &amp;quot;GET / HTTP/1.1&amp;quot; 200 OK
2026-05-31T17:14:42Z app[d89492dc3e5208] cdg [info]INFO:     172.16.45.10:59894 - &amp;quot;GET /favicon.ico HTTP/1.1&amp;quot; 404 Not Found
2026-05-31T17:16:57Z app[d89492dc3e5208] cdg [info]2026-05-31 17:16:57,535 ERROR Database connection failed
2026-05-31T17:16:57Z app[d89492dc3e5208] cdg [info]INFO:     172.16.45.10:52308 - &amp;quot;GET /error HTTP/1.1&amp;quot; 200 OK
2026-05-31T17:17:23Z app[d89492dc3e5208] cdg [info]2026-05-31 17:17:23,565 WARNING Rate limit approaching
2026-05-31T17:17:23Z app[d89492dc3e5208] cdg [info]INFO:     172.16.45.10:39662 - &amp;quot;GET /login HTTP/1.1&amp;quot; 200 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These logs are streamed in real time. To test this, leave the command running and make additional requests to the API. New log entries will appear immediately.&lt;/p&gt;
&lt;p&gt;While this is one way to keep an eye on your logs, this method is not really efficient when monitoring application issues during deployment because it requires you to watch your logs at all times. But it can sometimes be useful for debugging.&lt;/p&gt;
&lt;p&gt;If you work with multiple Fly applications, specify the application name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly logs -a my-fastapi-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures you only receive logs from the application you are interested in.&lt;/p&gt;
&lt;p&gt;Applications can run on multiple machines. You can see a list of your Machines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly machine list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can inspect a Machine using the Machine ID:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly machine status d89492dc3e5208
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful when troubleshooting issues affecting only a single Machine instance.&lt;/p&gt;
&lt;h3&gt;Viewing logs from the Fly.io dashboard&lt;/h3&gt;
&lt;p&gt;Fly.io also provides live tail logs through its web dashboard.&lt;/p&gt;
&lt;p&gt;Open your application in the Fly.io dashboard and navigate to the Logs section. Here you will see a view of your application&apos;s log stream, which can be convenient when you are away from your terminal or reviewing recent activity.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/fly-io-logging/fly-dashboard-logs.png&quot; alt=&quot;A screenshot of logs on the Fly.io dashboard&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For simple debugging tasks, the &lt;code&gt;fly logs&lt;/code&gt; command is the quickest way to inspect application activity. As your applications grow, log management becomes an important part of understanding application behavior and monitoring the health of your services, and tools like Honeybadger Insights help manage them effectively.&lt;/p&gt;
&lt;h2&gt;Shipping Fly.io logs to Honeybadger Insights&lt;/h2&gt;
&lt;p&gt;As mentioned earlier, Vector acts as a NATS client, and this is the basis of the Fly log shipper. Vector grabs the log and sends it to a location of your choosing. In this section, you will learn how to ship your &lt;a href=&quot;https://docs.honeybadger.io/guides/insights/integrations/fly-io/&quot;&gt;Fly.io logs to Honeybadger Insights&lt;/a&gt; using the Fly log shipper.&lt;/p&gt;
&lt;p&gt;To get started, we first need to create a new app for Fly log shipper. Run the following command to create a new directory and navigate into that directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir logshipper
cd logshipper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can create the log shipper app in the directory you just created:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly launch --no-deploy --image ghcr.io/superfly/fly-log-shipper:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are adding the &lt;code&gt;--no-deploy&lt;/code&gt; so it just creates and configures the app and does not deploy it, since we need to add a few configurations before deployment. &lt;code&gt;--image&lt;/code&gt; specifies the prebuilt image to be configured and later deployed.&lt;/p&gt;
&lt;p&gt;Now we will set some secrets. Create a &lt;a href=&quot;https://app.honeybadger.io/projects/new&quot;&gt;new project&lt;/a&gt; on Honeybadger and get the API key from that project. Setting &lt;code&gt;HONEYBADGER_API_KEY&lt;/code&gt; enables the shipping of logs to your Honeybadger project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly secrets set ORG=personal # The org you chose when running &amp;quot;fly launch&amp;quot;
fly secrets set ACCESS_TOKEN=$(fly auth token) # gets and sets Fly token 
fly secrets set HONEYBADGER_API_KEY=PROJECT_API_KEY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keeping your Fly tokens in secrets rather than hard-coding them helps protect access to your Fly.io infrastructure.&lt;/p&gt;
&lt;p&gt;Edit the generated &lt;code&gt;fly.toml&lt;/code&gt; file, replacing the entire &lt;code&gt;[http_service]&lt;/code&gt; section with this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[[services]]
  http_checks = []
  internal_port = 8686
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can now deploy the logger application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fly deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once that&apos;s done, you should see logs from your apps flowing into Insights. When you send requests to your deployed app, you will see the activity logged on to Honeybadger Insights.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/fly-io-logging/fly-logs-on-honeybadger.gif&quot; alt=&quot;A gif of Fly.io logging on Honeybadger Insights&quot; /&gt;&lt;/p&gt;
&lt;p&gt;On Honeybadger, you can run &lt;a href=&quot;https://docs.honeybadger.io/guides/insights/&quot;&gt;many queries&lt;/a&gt; on these logs, including searches like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;fields @ts, @preview
| filter fly.app.name::str == &amp;quot;fly-honeybadger&amp;quot;
| sort @ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this query, we&#x2019;ve piped the initial results (&lt;code&gt;fields @ts, @preview&lt;/code&gt;) through&#xa0;&lt;code&gt;filter&lt;/code&gt;, which accepts a variety of conditions. Here we have specified the data type of the&#xa0;&lt;code&gt;fly.app.name&lt;/code&gt;&#xa0;field as &lt;code&gt;str&lt;/code&gt; and compare to the string provided (&lt;code&gt;&amp;quot;fly-honeybadger&amp;quot;&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Basically, this is going through your logs and selecting the apps on Fly.io with the name &#x201c;fly-honeybadger&#x201d;. This can be helpful when you have multiple applications deployed on Fly.io.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/fly-io-logging/insights-query-result.png&quot; alt=&quot;A screenshot of query results on Honeybadger Insights&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;More on Honeybadger Insights&lt;/h2&gt;
&lt;p&gt;In this article, you saw how Fly.io captures everything your application writes to stdout and stderr, and how Fly&apos;s internal architecture (Vector &#x2192; NATS &#x2192; log shipper) makes it straightforward to route logs to external destinations. We then saw how to use Insights as a more well-rounded solution for logging.&lt;/p&gt;
&lt;p&gt;We only went through a surface level of what Insights is capable of when it comes to Fly.io logging, establishing a foundation for the deeper view into data analytics that Honeybadger enables. Having all that data available in one place reduces the time it takes to find answers, enabling you to easily:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Query and filter Fly.io logs using Honeybadger&apos;s powerful search language.&lt;/li&gt;
&lt;li&gt;Correlate logs with application errors or other application metrics.&lt;/li&gt;
&lt;li&gt;Monitor application behavior across multiple Fly.io deployments.&lt;/li&gt;
&lt;li&gt;Analyze your logs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are already using Fly.io in production, this article demonstrated one of the simplest ways to move from basic log viewing to a complete observability workflow with no additional code needed.&lt;/p&gt;
&lt;p&gt;Now that you know everything you need to about Fly.io logging, &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;sign up for a free Honeybadger account&lt;/a&gt; and start shipping your Fly.io logs today.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>August 2026 product update: hosted MCP and more</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/2026-august-product-update/"/>
    <id>https://www.honeybadger.io/blog/2026-august-product-update/</id>
    <published>2026-08-12T07:00:00+00:00</published>
    <updated>2026-08-12T07:00:00+00:00</updated>
    <author>
      <name>Joshua Wood</name>
    </author>
    <summary type="text">This cycle: OAuth on the hosted MCP server, EU support for self-hosting it, natural-language error search, anomaly alerts, and Oban-py support.</summary>
    <content type="html">&lt;p&gt;Your MCP client doesn&#x2019;t need your whole API key just to look up an error anymore.&lt;/p&gt;
&lt;p&gt;Honeybadger&apos;s hosted MCP server now supports OAuth. You can approve it through your browser, scope your permissions, revoke your permissions, and rest easy knowing that our tokens auto-refresh and don&#x2019;t sit around in a config.&lt;/p&gt;
&lt;p&gt;Keep reading to see how it works and get a quick recap of everything else that shipped this cycle.&lt;/p&gt;
&lt;h2&gt;MCP gets OAuth, and self-hosting gets more flexible&lt;/h2&gt;
&lt;p&gt;Until now, connecting an AI agent to Honeybadger&apos;s MCP server meant handing it a personal API token, giving it full account access and no way to reduce its scope. If you wanted to give Claude or Cursor read-only access to investigate errors, you were stuck granting it everything.&lt;/p&gt;
&lt;p&gt;The new hosted server fixes that. Add&#xa0;&lt;code&gt;https://mcp.honeybadger.io/mcp&lt;/code&gt;&#xa0;to your client and approve the connection in your browser &#x2014; no local install or token to manage.&lt;/p&gt;
&lt;p&gt;The first time a client connects, you pick an account and choose read-only or read-write access. Behind the scenes, Honeybadger issues short-lived tokens that refresh automatically, so there&apos;s no long-lived secret sitting in a config file. You can review or revoke any connected app anytime from&#xa0;&lt;strong&gt;User Settings &#x2192; API Access&lt;/strong&gt;&#xa0;(or, for admins, from any team member&apos;s account under&#xa0;&lt;strong&gt;Account Settings &#x2192; API Access&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Getting started:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;claude mcp add --transport http honeybadger &amp;quot;https://mcp.honeybadger.io/mcp&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cursor, Windsurf, and Claude Desktop&lt;/strong&gt;&#xa0;use a JSON config:&lt;/p&gt;
&lt;p&gt;json&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;mcpServers&amp;quot;: {
    &amp;quot;honeybadger&amp;quot;: {
      &amp;quot;url&amp;quot;: &amp;quot;https://mcp.honeybadger.io/mcp&amp;quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;VS Code&lt;/strong&gt;&#xa0;has its own CLI equivalent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code --add-mcp &apos;{&amp;quot;name&amp;quot;:&amp;quot;honeybadger&amp;quot;,&amp;quot;type&amp;quot;:&amp;quot;http&amp;quot;,&amp;quot;url&amp;quot;:&amp;quot;https://mcp.honeybadger.io/mcp&amp;quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EU accounts should use&#xa0;&lt;code&gt;https://eu-mcp.honeybadger.io/mcp&lt;/code&gt;&#xa0;instead. Each endpoint only accepts accounts from its own region.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;EU support is also here&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;If you want to use the self-hosted MCP with the EU stack, you can set&#xa0;&lt;code&gt;HONEYBADGER_API_URL&lt;/code&gt;&#xa0;to&#xa0;&lt;code&gt;https://eu-app.honeybadger.io&lt;/code&gt;&#xa0;in the&#xa0;&lt;code&gt;env&lt;/code&gt;&#xa0;block (and add a matching&#xa0;&lt;code&gt;-e HONEYBADGER_API_URL&lt;/code&gt;&#xa0;entry to&#xa0;&lt;code&gt;args&lt;/code&gt;&#xa0;for Docker, so the variable actually gets passed through), then authenticate with a personal auth token from your&#xa0;EU user settings. Keep in mind that a US token won&apos;t work against the EU region, and vice versa.&lt;/p&gt;
&lt;h3&gt;What to do with your MCP&lt;/h3&gt;
&lt;p&gt;Once connected, your agent can list and manage projects, search and filter errors, look at stack traces and affected users, run BadgerQL queries against Insights, and manage dashboards and alarms. The server also handles your agent reference material from BadgerQL and error search syntax automatically, so you don&apos;t need to paste documentation into your prompts.&lt;/p&gt;
&lt;p&gt;Full setup instructions, including client-specific configuration and building from source, are in the&#xa0;MCP documentation.&lt;/p&gt;
&lt;h2&gt;Also shipped this month&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Natural language search.&lt;/strong&gt;&#xa0;Error search and BadgerQL can answer almost any question about your errors and events, but only if you remember the syntax. Now you can describe what you want instead. Just click the lightbulb next to the search box, type something like &amp;quot;unresolved production errors from the last 24 hours that have comments,&amp;quot; and Honeybadger writes the query for you, editable and ready to run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anomaly detection.&lt;/strong&gt;&#xa0;Some of the worst incidents don&apos;t show up as one new error; they show up as a flood. Honeybadger now learns each project&apos;s normal error volume and alerts you when it spikes above that baseline &#x2014; no thresholds to configure. Available on Business and Enterprise plans.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Oban-py support.&lt;/strong&gt;&#xa0;Honeybadger now integrates with the Python port of Oban&#x2019;s background job library. Unhandled worker exceptions are reported automatically, per-job telemetry flows into Insights, and request context carries through to the jobs it enqueues.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That&apos;s everything from July. As always, the full history is on the&#xa0;changelog. If you&apos;re interested in trying out any of these features, a &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;Honeybadger account&lt;/a&gt; starts out as free for a single user.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Comprehensive guide to working with Python markdown</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/python-markdown/"/>
    <id>https://www.honeybadger.io/blog/python-markdown/</id>
    <published>2023-11-27T08:00:00+00:00</published>
    <updated>2026-08-03T07:00:00+00:00</updated>
    <author>
      <name>Ravgeet Dhillon</name>
    </author>
    <summary type="text">Markdown makes it easy to add syntax to your plain text documents for readability and machine parsing. Read to learn how to work with markdown in Python using the Python markdown package.</summary>
    <content type="html">&lt;p&gt;If you use the Internet, you have surely come across the term &lt;strong&gt;Markdown&lt;/strong&gt;. Markdown is a lightweight markup language that makes it very easy to write formatted content. It was created by John Gruber and Aaron Swartz in 2004. It uses very easy-to-remember syntax and is therefore used by many bloggers and content writers around the world. Even this blog that you are reading is written and formatted using Markdown.&lt;/p&gt;
&lt;p&gt;Markdown is one of the most widely used formats for storing formatted data. It easily integrates with Web technologies, as it can be converted to HTML or vice versa using Markdown compilers. It allows you to write HTML entities, such as headings, lists, images, links, tables, and more without much effort or code. It is used in blogs, content management systems, Wikis, documentation, and many more places.&lt;/p&gt;
&lt;p&gt;In this article, you&apos;ll learn how to work with Python markdown application using different Python packages, including markdown, front matter, and markdownify.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;To follow along with this tutorial, you&#x2019;ll need the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python v3.x&lt;/li&gt;
&lt;li&gt;Basic understanding of HTML and Markdown&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up a Project&lt;/h2&gt;
&lt;p&gt;Before proceeding with the project, you&#x2019;ll need to set up a project directory to work in.&lt;/p&gt;
&lt;p&gt;So, first, open up your terminal, navigate to a path of your choice, and create a project directory (&lt;code&gt;python-markdown&lt;/code&gt;) by running the following commands in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir python-markdown
cd python-markdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, create and activate the virtual environment (&lt;code&gt;venv&lt;/code&gt;) for your Python project by running the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m venv
source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#x2019;s it. The project setup is complete.&lt;/p&gt;
&lt;p&gt;By the way, if you are building Python web apps, we send practical Python, Django, and software engineering articles&#x2014;no hype, just useful stuff. &lt;a href=&quot;https://www.honeybadger.io/newsletter/&quot;&gt;Sign up for our newsletter&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Converting Markdown to HTML in Python&lt;/h2&gt;
&lt;p&gt;One of the most common operations related to Markdown is converting it to HTML. By doing so, you can write your content in Markdown and then compile it to HTML, which you can then deploy to a CDN or server.&lt;/p&gt;
&lt;p&gt;First, install the &lt;a href=&quot;https://pypi.org/project/Markdown/&quot;&gt;python-markdown&lt;/a&gt; package by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install markdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, at your project&#x2019;s root directory, create a &lt;code&gt;main.py&lt;/code&gt; file and add the following code to it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 1
import markdown

markdown_string = &apos;# Hello World&apos;

# 2
html_string = markdown.markdown(markdown_string)
print(html_string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above code, you are doing the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Importing the &lt;code&gt;markdown&lt;/code&gt; module.&lt;/li&gt;
&lt;li&gt;Converting the markdown (&lt;code&gt;markdown_string&lt;/code&gt;) to HTML (&lt;code&gt;html_string&lt;/code&gt;) using the &lt;code&gt;markdown&lt;/code&gt; method from the &lt;code&gt;markdown&lt;/code&gt; package.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, save your code and run the &lt;code&gt;main.py&lt;/code&gt; file by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the code execution is complete, you&#x2019;ll get the HTML output as follows:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/python-markdown/markdown_to_html.png&quot; alt=&quot;Markdown to HTML.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can try a more complex Markdown string like the one in the code below and use it to create HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;markdown_string = &apos;&apos;&apos;
# Hello World

This is a **great** tutorial about using Markdown in [Python](https://python.org).
&apos;&apos;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, you make use of headings, bold text, and links in Markdown.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/python-markdown/markdown_to_html_complex.png&quot; alt=&quot;Markdown to HTML.&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Converting a Markdown File to HTML in Python&lt;/h2&gt;
&lt;p&gt;Most of the time, you&#x2019;ll be working with Markdown files rather than Markdown strings. Therefore, it makes sense to learn how to convert a Markdown file to an HTML file.&lt;/p&gt;
&lt;p&gt;To do so, first, create a &lt;code&gt;sample.md&lt;/code&gt; file and add the following code to it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# Hello World

This is a **Markdown** file.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, replace the existing code in the &lt;code&gt;main.py&lt;/code&gt; file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import markdown

# 1
with open(&apos;sample.md&apos;, &apos;r&apos;) as f:
    markdown_string = f.read()

# 2
html_string = markdown.markdown(markdown_string)

# 3
with open(&apos;sample.html&apos;, &apos;w&apos;) as f:
    f.write(html_string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above code, you are doing the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reading the &lt;code&gt;sample.md&lt;/code&gt; and storing its content in the &lt;code&gt;markdown_string&lt;/code&gt; variable.&lt;/li&gt;
&lt;li&gt;Converting the markdown (&lt;code&gt;markdown_string&lt;/code&gt;) to HTML (&lt;code&gt;html_string&lt;/code&gt;) using the &lt;code&gt;markdown&lt;/code&gt; method from the &lt;code&gt;markdown&lt;/code&gt; package.&lt;/li&gt;
&lt;li&gt;Creating a &lt;code&gt;sample.html&lt;/code&gt; file and writing the HTML (&lt;code&gt;html_string&lt;/code&gt;) to it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, save your code and run the &lt;code&gt;main.py&lt;/code&gt; file by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the code execution is complete, you&#x2019;ll see a &lt;code&gt;sample.html&lt;/code&gt; file in your project&#x2019;s root directory:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/python-markdown/markdown_to_html_file.png&quot; alt=&quot;Markdown file to HTML file.&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Converting HTML to Markdown in Python&lt;/h2&gt;
&lt;p&gt;Sometimes, a situation arises where you might want to convert HTML to Markdown. For this purpose, you can use the markdownify package in Python.&lt;/p&gt;
&lt;p&gt;First, install the package by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pip install markdownify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, replace the existing code in the &lt;code&gt;main.py&lt;/code&gt; file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 1
import markdownify

html_string = &apos;&apos;&apos;
&amp;lt;h1&amp;gt;Hello World&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;This is a great tutorial about using Markdown in Python.&amp;lt;/p&amp;gt;
&apos;&apos;&apos;

# 2
markdown_string = markdownify.markdownify(html_string)
print(markdown_string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above code, you are doing the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Importing the &lt;code&gt;markdownify&lt;/code&gt; module.&lt;/li&gt;
&lt;li&gt;Converting the HTML (&lt;code&gt;html_string&lt;/code&gt;) to Markdown (&lt;code&gt;markdown_string&lt;/code&gt;) using the &lt;code&gt;markdownify&lt;/code&gt; method from the &lt;code&gt;markdownify&lt;/code&gt; package.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, save your code and run the &lt;code&gt;main.py&lt;/code&gt; file by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the code execution is complete, you&#x2019;ll get the Markdown output:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/python-markdown/html_to_markdown.png&quot; alt=&quot;HTML to Markdown.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you see the output above, you&#x2019;ll see the headings (&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;) created with the &amp;quot;underlining&amp;quot; with equal signs (=) instead of starting with hashtags (#). This is because Markdown comes with two styles of headers: &lt;strong&gt;Setext&lt;/strong&gt; and &lt;strong&gt;atx&lt;/strong&gt;, and by default, the Markdown parser uses Setext-style headers. You configure markdownify to use ATX-style headers by passing the &lt;code&gt;heading_style=&apos;ATX&apos;&lt;/code&gt; parameter to the &lt;code&gt;markdownify&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;Markdownify also supports a number of options, including HTML tag stripping, HTML tag conversion, Markdown heading styles, and more.&lt;/p&gt;
&lt;h2&gt;Converting an HTML File to Markdown in Python&lt;/h2&gt;
&lt;p&gt;Previously, we converted a Markdown file to an HTML file. However, sometimes, you might need to convert an HTML file to a Markdown file.&lt;/p&gt;
&lt;p&gt;To do so, first, create a &lt;code&gt;sample.html&lt;/code&gt; file and add the following code to it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Hello World&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;This is a &amp;lt;strong&amp;gt;HTML&amp;lt;/strong&amp;gt; file.&amp;lt;/p&amp;gt;
    &amp;lt;a href=&amp;quot;https://honeybadger.io/&amp;quot;&amp;gt;Visit Honeybadger&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, replace the existing code in the &lt;code&gt;main.py&lt;/code&gt; file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import markdownify

# 1
with open(&apos;sample.html&apos;, &apos;r&apos;) as f:
    html_string = f.read()

# 2
markdown_string = markdownify.markdownify(html_string, heading_style=&apos;ATX&apos;)

# 3
with open(&apos;sample.md&apos;, &apos;w&apos;) as f:
    f.write(markdown_string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above code, you&#x2019;re doing the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reading the &lt;code&gt;sample.html&lt;/code&gt; and storing its content in the &lt;code&gt;html_string&lt;/code&gt; variable.&lt;/li&gt;
&lt;li&gt;Converting the HTML (&lt;code&gt;html_string&lt;/code&gt;) to Markdown (&lt;code&gt;markdown_string&lt;/code&gt;) using the &lt;code&gt;markdownify&lt;/code&gt; method from the &lt;code&gt;markdownify&lt;/code&gt; package.&lt;/li&gt;
&lt;li&gt;Creating a &lt;code&gt;sample.md&lt;/code&gt; file and writing the Markdown (&lt;code&gt;markdown_string&lt;/code&gt;) to it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, save your code and run the &lt;code&gt;main.py&lt;/code&gt; file by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the code execution is complete, you&#x2019;ll see a &lt;code&gt;sample.md&lt;/code&gt; file in your project&#x2019;s root directory as follows:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/python-markdown/html_to_markdown_file.png&quot; alt=&quot;HTML file to Markdown file.&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Using Front Matter for Python markdown&lt;/h2&gt;
&lt;p&gt;In the world of markdown, there are often some variables or metadata associated with a Markdown file. This is known as &lt;strong&gt;front matter&lt;/strong&gt;. Front matter data variables are a great way to store extra information about a Markdown file. For example, a blog&#x2019;s markdown files can have front matter variables like &lt;em&gt;Title&lt;/em&gt;, &lt;em&gt;Author&lt;/em&gt;, &lt;em&gt;Image&lt;/em&gt;, &lt;em&gt;Published At&lt;/em&gt;, and more.&lt;/p&gt;
&lt;p&gt;You can specify front matter at the beginning of a Markdown file by placing the YAML data variables between triple-dashed lines. For example,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
title: Hello World
Author: John Doe
Published: 2020-01-20
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Python, you can parse Markdown front matter with the python-front matter package.&lt;/p&gt;
&lt;p&gt;To see this package in action, first, install the package by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install python-frontmatter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, add the following front matter to the &lt;code&gt;sample.md&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
title: Hello World
date: 2022-01-20
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, replace the existing code in the &lt;code&gt;main.py&lt;/code&gt; file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 1
import frontmatter

# 2
data = frontmatter.load(&apos;sample.md&apos;)

# 3
print(data.keys())
print(data[&apos;title&apos;])
print(data[&apos;date&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above code, you are doing the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Importing the &lt;code&gt;frontmatter&lt;/code&gt; module.&lt;/li&gt;
&lt;li&gt;Reading the &lt;code&gt;sample.md&lt;/code&gt; file using the &lt;code&gt;load&lt;/code&gt; method from the &lt;code&gt;frontmatter&lt;/code&gt; package and storing the result in the &lt;code&gt;data&lt;/code&gt; variable.&lt;/li&gt;
&lt;li&gt;Accessing the front matter variables with the help of &lt;code&gt;data.keys()&lt;/code&gt;. Since &lt;code&gt;data&lt;/code&gt; is a dictionary, you can also access the individual keys (&lt;code&gt;data[&apos;title&apos;]&lt;/code&gt; or &lt;code&gt;data[&apos;date&apos;]&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, save your code and run the &lt;code&gt;main.py&lt;/code&gt; file by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the code execution is complete, you&#x2019;ll get the output of the front matter variables as follows:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/python-markdown/frontmatter_data.png&quot; alt=&quot;Markdown front matter data.&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Updating Markdown Front Matter in Python&lt;/h2&gt;
&lt;p&gt;Sometimes, a situation arises where you might want to convert HTML to Markdown. For this purpose, you can use the Python&#x2019;s &lt;a href=&quot;https://pypi.org/project/markdownify/0.4.0/&quot;&gt;markdownify&lt;/a&gt; package.&lt;/p&gt;
&lt;p&gt;You can also update the existing front matter data variables or add new ones using the front matter package.&lt;/p&gt;
&lt;p&gt;To do so, first, replace the existing code in the &lt;code&gt;main.py&lt;/code&gt; file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import frontmatter

# 1
data = frontmatter.load(&apos;sample.md&apos;)

# 2
data[&apos;author&apos;] = &apos;John Doe&apos;

# 3
data[&apos;title&apos;] = &apos;Bye World&apos;

# 4
updated_data = frontmatter.dumps(data)

# 5
with open(&apos;sample.md&apos;, &apos;w&apos;) as f:
    f.write(updated_data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above code, you are doing the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reading (&lt;code&gt;frontmater.load()&lt;/code&gt;) the &lt;code&gt;sample.md&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Adding a new key (&lt;code&gt;author&lt;/code&gt;) to the front matter &lt;code&gt;data&lt;/code&gt; variable and assigning it a value (&lt;code&gt;John Doe&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Updating the existing key (&lt;code&gt;title&lt;/code&gt;) and assigning it a new value (&lt;code&gt;Bye World&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Serializing (&lt;code&gt;frontmatter.dumps()&lt;/code&gt;) the &lt;code&gt;data&lt;/code&gt; variable to a &lt;em&gt;string&lt;/em&gt; and storing the result in the &lt;code&gt;updated_data&lt;/code&gt; variable.&lt;/li&gt;
&lt;li&gt;Updating the &lt;code&gt;sample.md&lt;/code&gt; file by writing the updated Markdown (&lt;code&gt;updated_data&lt;/code&gt;) to it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, save your code and run the &lt;code&gt;main.py&lt;/code&gt; file by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the code execution is complete, check the &lt;code&gt;sample.md&lt;/code&gt; file for the updated front matter data, as follows:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/python-markdown/frontmatter_data_update.png&quot; alt=&quot;Updated Markdown front matter data.&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Using Python Markdown Extensions&lt;/h2&gt;
&lt;p&gt;The python-markdown package also supports extensions that allow you to modify and/or extend the default behavior of the Markdown parser. For example, to generate a table of contents (TOC), you can use the toc extension. There are &lt;a href=&quot;https://python-markdown.github.io/extensions/&quot;&gt;other extensions&lt;/a&gt;, as well, which you can make use of based on your requirements.&lt;/p&gt;
&lt;p&gt;To create a TOC for your Markdown content, first, replace the existing code in the &lt;code&gt;main.py&lt;/code&gt; file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import markdown

# 1
markdown_string = &apos;&apos;&apos;
[TOC]

# Hello World

This is a **great** tutorial about using Markdown in [Python](https://python.org).

# Bye World
&apos;&apos;&apos;

# 2
html_string = markdown.markdown(markdown_string, extensions=[&apos;toc&apos;])
print(html_string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the above code, you are doing the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Specifying the &lt;code&gt;[TOC]&lt;/code&gt; string in your Markdown (&lt;code&gt;markdown_string&lt;/code&gt;) where you want to add the table of contents.&lt;/li&gt;
&lt;li&gt;Adding the &lt;code&gt;extensions&lt;/code&gt; parameter to the &lt;code&gt;markdown&lt;/code&gt; method from the &lt;code&gt;markdown&lt;/code&gt; package and specifying the extensions (&lt;code&gt;[&apos;toc&apos;]&lt;/code&gt;) you want to use.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, save your code and run the &lt;code&gt;main.py&lt;/code&gt; file by running the following command in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the code execution is complete, you&#x2019;ll get the HTML output with the Table of Contents as a list:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/python-markdown/table_of_contents.png&quot; alt=&quot;Python markdown table of Contents.&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;where do you go from here?&lt;/h2&gt;
&lt;p&gt;Learning to work with Markdown can help you in lots of ways. Using this guide as the basis, you can automate many tasks, including maintaining and manipulating Markdown files. For example, you can write a script that creates an index for all of your Python markdown files in your blog or organize your markdown files into different directories based on the front matter data variables, such as tags/categories.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.honeybadger.io/for/python/&quot;&gt;Honeybadger&lt;/a&gt;, which is a cloud-based system for real-time monitoring, error tracking, and exception-catching, also uses Markdown to maintain our documentation. In case you are interested, we wrote a blog post in which we talk about how we &lt;a href=&quot;https://www.honeybadger.io/blog/documentation-worklow-rails/&quot;&gt;built a documentation workflow in Rails&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Starlette vs FastAPI: what FastAPI actually adds</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/starlette-vs-fastapi/"/>
    <id>https://www.honeybadger.io/blog/starlette-vs-fastapi/</id>
    <published>2026-07-20T07:00:00+00:00</published>
    <updated>2026-07-20T07:00:00+00:00</updated>
    <author>
      <name>Farhan Hasin Chowdhury</name>
    </author>
    <summary type="text">FastAPI is built on Starlette, but most developers never look at what&apos;s underneath. Learn what FastAPI actually adds on top of Starlette and Pydantic, what comes straight from Starlette, and when dropping down to raw Starlette makes more sense than pulling in the full stack.</summary>
    <content type="html">&lt;p&gt;FastAPI has become one of the most popular web frameworks among Python developers. It&apos;s so popular that it often overshadows the technologies it&apos;s built on. FastAPI is built on Starlette and Pydantic. Starlette handles the HTTP layer (routing, middleware, WebSockets, the ASGI plumbing) and Pydantic handles data validation. FastAPI is the layer on top that ties them together with type-driven parameter parsing, dependency injection, and automatic OpenAPI documentation.&lt;/p&gt;
&lt;p&gt;That overshadowing is why the Starlette vs FastAPI choice is often framed as choosing between direct competitors, as if you have to pick sides. You don&apos;t. The real question is what FastAPI adds and when you might skip it. In this article, I&apos;ll walk through the parts of a FastAPI application that come straight from Starlette, the parts FastAPI adds on top, and a few cases where dropping down to raw Starlette is the better call.&lt;/p&gt;
&lt;h2&gt;What Starlette and FastAPI actually are&lt;/h2&gt;
&lt;p&gt;Starlette is a lightweight ASGI toolkit. ASGI is the asynchronous successor to WSGI, and it&apos;s the spec that lets Python web servers like Uvicorn and Hypercorn talk to async applications. Starlette consists of a routing system, request and response objects, a middleware pipeline, WebSocket support, background tasks, sessions, a test client, and a small set of built-in middlewares for things like CORS and GZip. That&apos;s everything you need to build an async web service against the spec, small and unopinionated by design.&lt;/p&gt;
&lt;p&gt;Pydantic is the other half of the foundation. It&apos;s a general-purpose data validation library: you declare a class with typed fields, and Pydantic handles parsing, validation, serialization, and JSON Schema generation for free. FastAPI happens to use it. So does anything else that needs typed data shapes.&lt;/p&gt;
&lt;p&gt;FastAPI is a thin layer that hooks into Starlette and Pydantic through your function signatures; when you write &lt;code&gt;def create_user(user: User)&lt;/code&gt;, FastAPI sees the Pydantic model in the type hint and wires up Starlette&apos;s request body parsing to it. That one mechanism is the foundation of everything FastAPI does: parameter parsing, validation, and OpenAPI schema generation are all derivatives of reading your type hints. The docs and dependency injection are conveniences built on top.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/starlette-vs-fast-api/architecture.png&quot; alt=&quot;Layered architecture: your application code sits on FastAPI, which sits on Starlette and Pydantic, which sit on the ASGI app (Uvicorn)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The diagram above is the mental model worth keeping. Whenever you write a FastAPI app, you&apos;re really writing code that sits on FastAPI, which sits on Starlette and Pydantic, which talk to an ASGI server underneath.&lt;/p&gt;
&lt;h2&gt;Starlette vs FastAPI: core differences&lt;/h2&gt;
&lt;p&gt;The two frameworks make fundamentally different tradeoffs. Starlette gives you low-level HTTP building blocks: routing, middleware, and WebSockets, without prescribing structure. FastAPI adds a declarative layer on top: automatic validation, serialization, interactive docs, and dependency injection, all driven by standard Python type hints.&lt;/p&gt;
&lt;p&gt;With Starlette, you wire up validation and injection yourself or skip them. With FastAPI, you declare what you need in the function signature and get validation and failure responses for free.&lt;/p&gt;
&lt;p&gt;Both share the same async runtime. The difference is how much boilerplate you need to write.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/starlette-vs-fast-api/comparison-diagram.png&quot; alt=&quot;Starlette vs FastAPI comparison diagram showing Starlette as the low-level ASGI app foundation and FastAPI as the high-level API framework layer&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Performance and async capabilities&lt;/h2&gt;
&lt;p&gt;FastAPI gets its &lt;code&gt;async def&lt;/code&gt; support from Starlette; there&apos;s no separate event loop or async runtime. A FastAPI &lt;code&gt;async def&lt;/code&gt; endpoint uses Starlette&apos;s ASGI integration the same way a raw Starlette endpoint does.&lt;/p&gt;
&lt;p&gt;A Starlette &lt;code&gt;async def&lt;/code&gt; endpoint looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

async def homepage(request):
    return JSONResponse({&amp;quot;hello&amp;quot;: &amp;quot;world&amp;quot;})

app = Starlette(routes=[Route(&amp;quot;/&amp;quot;, homepage)])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A FastAPI &lt;code&gt;async def&lt;/code&gt; endpoint looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import FastAPI

app = FastAPI()

@app.get(&amp;quot;/&amp;quot;)
async def homepage():
    return {&amp;quot;hello&amp;quot;: &amp;quot;world&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both run on the same ASGI app and event loop. The async I/O behavior is identical. FastAPI&apos;s decorator adds route registration, serialization, and OpenAPI schema generation. Starlette gives you the route and response object.&lt;/p&gt;
&lt;p&gt;However, FastAPI handles sync functions differently. Declare a path operation with &lt;code&gt;def&lt;/code&gt; instead of &lt;code&gt;async def&lt;/code&gt;, and FastAPI runs it in a threadpool&#x2014;so a slow call doesn&apos;t stall the event loop. Starlette does the same, but FastAPI extends this to dependencies too, which matters once you use its injection system.&lt;/p&gt;
&lt;p&gt;The threadpool isn&apos;t infinite. Both frameworks use AnyIO, which by default caps the pool at 40 worker threads. A few slow sync handlers under load can saturate it, causing the threadpool to queue requests. Mixing &lt;code&gt;def&lt;/code&gt; and &lt;code&gt;async def&lt;/code&gt; without considering the thread pool is a common reason a FastAPI app feels fast in testing but slow under load. The fix is usually to push the slow work into a background worker, not to bump the threadpool limit.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/starlette-vs-fast-api/asgi-stack.png&quot; alt=&quot;ASGI application stack showing Uvicorn, Starlette, and FastAPI layers&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Middleware: same stack, same tools&lt;/h2&gt;
&lt;p&gt;Middleware is one of the clearest examples of FastAPI sitting on Starlette without modification. The middleware pipeline, the base classes, and the bundled middlewares all come from Starlette. FastAPI just re-exports them.&lt;/p&gt;
&lt;p&gt;A custom middleware in Starlette looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from starlette.middleware.base import BaseHTTPMiddleware

class TimingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        import time
        start = time.perf_counter()
        response = await call_next(request)
        response.headers[&amp;quot;X-Process-Time&amp;quot;] = f&amp;quot;{time.perf_counter() - start:.4f}&amp;quot;
        return response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same class drops straight into a FastAPI app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import FastAPI

app = FastAPI()
app.add_middleware(TimingMiddleware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works because FastAPI implements the ASGI spec through Starlette, so any ASGI middleware works in either framework. The built-in &lt;code&gt;CORSMiddleware&lt;/code&gt;, &lt;code&gt;GZipMiddleware&lt;/code&gt;, &lt;code&gt;TrustedHostMiddleware&lt;/code&gt;, and &lt;code&gt;SessionMiddleware&lt;/code&gt; you reach for in a FastAPI app all live in the &lt;code&gt;starlette.middleware&lt;/code&gt; package.&lt;/p&gt;
&lt;p&gt;Two Starlette quirks apply unchanged in FastAPI. First, middleware is registered in LIFO order, so the last &lt;code&gt;add_middleware&lt;/code&gt; call runs first on the way in and last on the way out.&lt;/p&gt;
&lt;p&gt;Second, &lt;code&gt;BaseHTTPMiddleware&lt;/code&gt; doesn&apos;t propagate &lt;code&gt;contextvars&lt;/code&gt; changes to the rest of the request. If you&apos;re doing distributed tracing or request-scoped logging, write a raw ASGI middleware instead.&lt;/p&gt;
&lt;p&gt;For production, you need middleware that catches errors. A raw 500 doesn&apos;t tell you much. A thin wrapper is the cleanest place to capture the stack trace, URL, and request context before sending the response. Here&apos;s an example that reports unhandled errors to &lt;a href=&quot;https://www.honeybadger.io/for/python/&quot;&gt;Honeybadger&lt;/a&gt;, which notifies your team immediately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from starlette.middleware.base import BaseHTTPMiddleware
from honeybadger import honeybadger

class HoneybadgerMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        try:
            return await call_next(request)
        except Exception as exc:
            honeybadger.notify(exc, context={
                &amp;quot;path&amp;quot;: request.url.path,
                &amp;quot;method&amp;quot;: request.method,
            })
            raise

app.add_middleware(HoneybadgerMiddleware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the middleware contract is Starlette&apos;s, the same code works in both frameworks without modification. If you ever migrate a service from FastAPI to bare Starlette (or vice versa), the middleware remains unchanged.&lt;/p&gt;
&lt;p&gt;If you&apos;re using the Honeybadger Python SDK, you don&apos;t need to write this yourself. The SDK ships with a &lt;a href=&quot;https://docs.honeybadger.io/lib/python/integrations/other/#starlette&quot;&gt;built-in Starlette middleware&lt;/a&gt; that catches exceptions, attaches request context, and sends everything to Honeybadger.&lt;/p&gt;
&lt;h2&gt;WebSocket support&lt;/h2&gt;
&lt;p&gt;WebSockets are another part of FastAPI that&apos;s almost entirely Starlette underneath. The &lt;code&gt;WebSocket&lt;/code&gt; object, the connection lifecycle (&lt;code&gt;accept&lt;/code&gt;, &lt;code&gt;receive_text&lt;/code&gt;, &lt;code&gt;send_json&lt;/code&gt;, &lt;code&gt;close&lt;/code&gt;), and the &lt;code&gt;WebSocketDisconnect&lt;/code&gt; exception all come from &lt;code&gt;starlette.websockets&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;FastAPI re-exports them and adds the same dependency injection and parameter parsing it adds to HTTP routes.&lt;/p&gt;
&lt;p&gt;A FastAPI WebSocket endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

@app.websocket(&amp;quot;/ws&amp;quot;)
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    try:
        async for message in websocket.iter_text():
            await websocket.send_text(f&amp;quot;Echo: {message}&amp;quot;)
    except WebSocketDisconnect:
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apart from the decorator, every line is Starlette code. The Starlette version uses &lt;code&gt;WebSocketRoute&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;What FastAPI adds is dependency injection: you can pull a database session, authenticated user, or query parameter into a WebSocket handler the same way you would in an HTTP route.&lt;/p&gt;
&lt;p&gt;If your service is mostly WebSocket-driven and you don&apos;t need OpenAPI docs for HTTP routes, a Starlette application gets you the same capabilities with less to install.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/starlette-vs-fast-api/websocket-lifecycle.png&quot; alt=&quot;WebSocket connection lifecycle diagram showing accept, send, and receive phases&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Data validation and serialization&lt;/h2&gt;
&lt;p&gt;This is where the two frameworks feel different. Starlette doesn&apos;t know anything about Pydantic. You can absolutely use them together, but you do the wiring by hand. FastAPI&apos;s defining feature is that it handles the wiring for you, using your function signature and standard Python type hints.&lt;/p&gt;
&lt;p&gt;Here&apos;s the same &amp;quot;create a user&amp;quot; endpoint written both ways. With Starlette and Pydantic, you&apos;re responsible for parsing the request data, calling Pydantic, and converting validation errors into the right failure response for incoming requests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from pydantic import BaseModel, ValidationError
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

class User(BaseModel):
    name: str
    email: str
    age: int

async def create_user(request):
    payload = await request.json()
    try:
        user = User(**payload)
    except ValidationError as exc:
        return JSONResponse({&amp;quot;errors&amp;quot;: exc.errors()}, status_code=422)
    return JSONResponse(user.model_dump(), status_code=201)

app = Starlette(routes=[Route(&amp;quot;/users&amp;quot;, create_user, methods=[&amp;quot;POST&amp;quot;])])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The FastAPI version is shorter because the framework infers all of the above from the type hint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from fastapi import FastAPI
from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str
    age: int

app = FastAPI()

@app.post(&amp;quot;/users&amp;quot;, status_code=201)
async def create_user(user: User):
    return user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FastAPI sees the &lt;code&gt;User&lt;/code&gt; parameter, parses the incoming JSON data, validates it against the Pydantic data model, returns a 422 response with structured error details if validation fails, and serializes the return value back to JSON.&lt;/p&gt;
&lt;p&gt;It works in reverse, too. With a &lt;code&gt;response_model&lt;/code&gt;, FastAPI validates and filters your return value before sending it, preventing database columns from leaking to API clients without requiring a custom serialization layer. Starlette leaves output filtering to you.&lt;/p&gt;
&lt;p&gt;Both frameworks handle nested Pydantic models. The difference is who does the wiring.&lt;/p&gt;
&lt;p&gt;That&apos;s &amp;quot;FastAPI built on Starlette and Pydantic&amp;quot; in practice. Starlette provides the HTTP objects, Pydantic provides validation, and FastAPI connects your function signature to both.&lt;/p&gt;
&lt;p&gt;The cost is mostly conceptual: more magic between the request and your function. For most APIs, that&apos;s a good trade. When you need full control over request parsing, Starlette is cleaner.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/starlette-vs-fast-api/validation-flow.png&quot; alt=&quot;Data validation flow diagram showing request data entering Pydantic validation and returning either a validated data model or an error response&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Automatic documentation generation&lt;/h2&gt;
&lt;p&gt;For many teams, this is the one feature that settles it. Starlette doesn&apos;t generate API docs. You can wire up swagger-ui or apispec yourself, but nothing is built in.&lt;/p&gt;
&lt;p&gt;FastAPI generates an OpenAPI 3.1 schema automatically from your path operations and Pydantic models, and serves two interactive doc UIs out of the box:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Swagger UI at &lt;code&gt;/docs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ReDoc at &lt;code&gt;/redoc&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It helps to be clear about who does what. Pydantic generates a JSON Schema for each data model. FastAPI assembles those schemas into a full OpenAPI document per the OpenAPI standard, along with path info from your decorators (URLs, methods, status codes, tags), and serves the result via Swagger UI and ReDoc. Without Pydantic, FastAPI would have no schemas to embed. Without FastAPI, you&apos;d have schemas but nothing tying them to URLs.&lt;/p&gt;
&lt;p&gt;The schema includes parameter types, request and response data models, validation constraints, status codes, and any descriptions you add. There&apos;s no separate file to maintain and no annotations to keep in sync. Because the schema is derived from the same type hints that drive validation, it can&apos;t drift out of date with your endpoints.&lt;/p&gt;
&lt;p&gt;For internal services, this turns the API itself into the docs. For public APIs, the OpenAPI schema can be used to generate client code, contract testing tools, and API gateways. Replicating this automatic documentation in a Starlette application is doable but noticeable work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/starlette-vs-fast-api/swagger-ui.png&quot; alt=&quot;Swagger UI screenshot showing automatically generated API documentation for a FastAPI application&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;When to use Starlette directly&lt;/h2&gt;
&lt;p&gt;Most projects are well served by FastAPI. I&apos;d reach for Starlette directly only where the FastAPI layer would mostly sit unused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Webhook receivers and lightweight proxies.&lt;/strong&gt; If your service does little more than accept a payload, validate a signature, and forward it somewhere, the OpenAPI schema and Pydantic-driven validation aren&apos;t earning their keep. A Starlette application with a couple of routes is smaller, starts faster, and has fewer moving parts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket-heavy services.&lt;/strong&gt; If your application is mostly WebSocket connections with little or no REST surface, FastAPI&apos;s HTTP-focused additions don&apos;t apply to most of your code. Starlette&apos;s WebSocket primitives are exactly what FastAPI uses anyway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom request handling.&lt;/strong&gt; If you need to stream a request body, parse a non-standard content type, or short-circuit before the body is fully read, FastAPI&apos;s parameter parsing can fight you. Starlette&apos;s lower-level request object gives you direct control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maximum minimalism.&lt;/strong&gt; Smaller dependency footprint, fewer abstractions, faster cold starts. This can make a real difference for serverless functions or container images you ship frequently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For everything else &#x2014; typical CRUD APIs, internal services, anything where automatic docs and validation pull their weight &#x2014; FastAPI&apos;s layer is worth the trade.&lt;/p&gt;
&lt;h2&gt;How the layers fit together&lt;/h2&gt;
&lt;p&gt;Here&apos;s where each layer takes over during a typical request:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/starlette-vs-fast-api/request-lifecycle.png&quot; alt=&quot;Request lifecycle showing how a request flows from Uvicorn through Starlette routing and middleware, into FastAPI&apos;s parameter parsing and Pydantic validation, then back out&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Uvicorn receives bytes off the wire and translates them into ASGI events. Starlette runs the middleware stack and matches the route. FastAPI&apos;s layer parses data from the request, validates it with Pydantic, calls your function, and serializes the return data. Then control hands back to Starlette to send the response, and to Uvicorn to write it to the socket.&lt;/p&gt;
&lt;p&gt;Almost every line of that flow is Starlette&apos;s, with FastAPI bracketed in the middle to handle the type-driven work.&lt;/p&gt;
&lt;h2&gt;Starlette vs FastAPI: where the layers end&lt;/h2&gt;
&lt;p&gt;&amp;quot;FastAPI built on Starlette and Pydantic&amp;quot; is more than a tagline. It&apos;s an architectural decision that shows up everywhere in the Starlette vs FastAPI stack. The async support, middleware, WebSockets, routing, and request and response objects all come from Starlette unchanged. Pydantic does the validation. FastAPI wires your function signatures to both, plus dependency injection and OpenAPI on top.&lt;/p&gt;
&lt;p&gt;Once you know where the seams are, a lot of things get easier: debugging middleware, picking a framework for a small service, reading stack traces, and understanding what&apos;s running underneath your code.&lt;/p&gt;
&lt;p&gt;Whichever side of the Starlette vs FastAPI stack you end up on, you&apos;ll still need to know when things break in production. Honeybadger catches unhandled exceptions in your FastAPI or Starlette app, groups duplicates, and notifies you with the request context attached. &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;Sign up for a free developer account&lt;/a&gt; and start monitoring your apps.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How a status page can show a site at its best (and 10 examples)</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/status-page-examples/"/>
    <id>https://www.honeybadger.io/blog/status-page-examples/</id>
    <published>2026-07-15T07:00:00+00:00</published>
    <updated>2026-07-15T07:00:00+00:00</updated>
    <author>
      <name>James Konik</name>
    </author>
    <summary type="text">A status page is an essential tool for keeping users updated, but how can you make sure yours is the best possible? In this article you&apos;ll see how to make your status page perfect, and how Honeybadger can help you do that.</summary>
    <content type="html">&lt;p&gt;Your status page is a bridge to your customers. It&#x2019;s where you show what you can do and prove that your product is more than just sales talk. Though easy to overlook, it provides an opportunity to showcase all that&#x2019;s best about your services.&lt;/p&gt;
&lt;p&gt;The trick is how to do it well. Fortunately, there are many outstanding status pages that you can draw inspiration from. Once your ideas take shape, you&apos;re ready to present your vision to customers.&lt;/p&gt;
&lt;p&gt;In this article, you&#x2019;ll learn what a public status page can do, why you should have one, and what it takes to make a good one. After that, we&apos;ll run through some status page examples that show what others have achieved with their pages, and then we&apos;ll show how easy it is to set up a status page using Honeybadger.&lt;/p&gt;
&lt;h2&gt;Why is it important to have a public status page?&lt;/h2&gt;
&lt;p&gt;Using your services is an act of faith that you need to earn and then repay. Customers want to know they can trust you, and a public status page lets you demonstrate your reliability. If you&#x2019;re achieving 100% uptime, it&#x2019;s the place to show it off.&lt;/p&gt;
&lt;p&gt;It&#x2019;s a plain-speaking, fact-driven demonstration of your commitment to your customers and a showcase of your ability to deliver a useful, usable product. Your status and incident communication skills are a key part of customer relationship management.&lt;/p&gt;
&lt;p&gt;Strong metrics on your page show that you can help your customers achieve their own goals. However, when things do go wrong, being transparent about it via a dedicated status page makes it less disruptive. It&#x2019;s less of a shock if users understand the situation or receive advance notice of scheduled maintenance. They can see what&#x2019;s happening and that you&apos;re working to resolve the issue.&lt;/p&gt;
&lt;h2&gt;What should a strong status page include?&lt;/h2&gt;
&lt;p&gt;A good status page isn&#x2019;t just about looking good. It&#x2019;s about presenting information your clients need, and helping them find it. Here are some points you should consider when designing your page.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Clarity: The main function of a status page is to let users know that your services are running. Function takes precedence over form, though it never hurts if your site is pleasant to look at. Easily readable and findable information ensures your users have a positive experience.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Metrics: Uptime is the key metric for a status page, but general service health and response times matter too. You don&#x2019;t have to be comprehensive, but presenting additional data can help. Showing planned maintenance or scheduled downtime is also useful, as is providing date switchers to change the displayed periods.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Links: The dual goals of presenting information while keeping things simple can be at odds with each other. Links can provide further information to those who need it without cluttering things for everyone else. A link to your support page or to any of your other online services is also a good idea.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;10 great status page examples&lt;/h2&gt;
&lt;p&gt;Now that we&apos;ve looked at the theory, let&apos;s take a look at some of the best status page examples. These great examples show the system status for various services, displaying metrics like historical uptime, response times, and third-party service info. Most have a user-friendly interface and show detailed status updates whenever there&apos;s a change. Studying these can help you decide what to include on your own page.&lt;/p&gt;
&lt;h3&gt;Wistia&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/wistia.png&quot; alt=&quot;Wistia status page showing uptime and media processing time.&quot; /&gt;
&lt;em&gt;As well as uptime, Wistia shows you its media processing time history&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://status.wistia.com/&quot;&gt;Wistia&lt;/a&gt; is a video management platform for those looking for something a little more businesslike than YouTube. Its status page packs in plenty of information, but still manages to be clear and readable. You can see what&#x2019;s happened over the last 90 days, with more detail available by mousing over each day, and there are options to change the period shown. There&#x2019;s also a useful graph showing media processing wait time. The page also includes incident reporting and various other information channels.&lt;/p&gt;
&lt;p&gt;Strengths: Comprehensive. Readable. Crisp.&lt;/p&gt;
&lt;h3&gt;Okta&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/okta.png&quot; alt=&quot;Okta&#x2019;s status page, showing clipped tick at top, smaller ticks, and an outage calendar at the bottom.&quot; /&gt;
&lt;em&gt;Okta&#x2019;s page includes a reassuring tick, and a calendar showing service disruptions&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://status.okta.com/&quot;&gt;Okta&lt;/a&gt; is an identity and access management platform. Its site strikes a useful balance between simplicity and detail. There&#x2019;s a nice big tick to quickly show that things are working, followed by a single page of alphabetically listed services that provide more specific information.&lt;/p&gt;
&lt;p&gt;There&#x2019;s an unusual calendar display that shows you what&#x2019;s happened over the past month, too. The site&#x2019;s clear design and color coding show you what&#x2019;s going on without you needing to figure anything out. There are also various useful links.&lt;/p&gt;
&lt;p&gt;Strengths: Straightforward. Original. Detailed.&lt;/p&gt;
&lt;h3&gt;Flare&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/flare.png&quot; alt=&quot;Flare status page showing recent uptime, and current status of services&quot; /&gt;
&lt;em&gt;Flare&#x2019;s status page makes it very easy to check uptime and performance-related information&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://status.flare.io/&quot;&gt;Flare&lt;/a&gt; is a cybersecurity tool that helps ensure user identities can be trusted. Like all good public status pages, it lets you see at a glance how its various services are performing. Mousing over any particular date brings up a pop-up with more information, though the animation is slightly off-putting. You can also flip the display between uptime and performance, and there are easy controls to change the displayed period.&lt;/p&gt;
&lt;p&gt;Strengths: Interactive. Adjustable. Powerful.&lt;/p&gt;
&lt;h3&gt;Deno&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/deno.png&quot; alt=&quot;Deno&#x2019;s status page showing various services are operational.&quot; /&gt;
&lt;em&gt;Deno&#x2019;s minimal style oozes calmness and authority&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://denostatus.com/&quot;&gt;Deno&lt;/a&gt; is an open-source JavaScript runtime with a status page that reflects the technical prowess of its product. As well as looking sleek, cold, and moody, the page&apos;s logo speaks to our affinity for stylish animals. The page is very clear and provides plenty of additional information via expanders. Its informational pop-ups work well. We suspect whoever designed this knows their CSS stuff, and perhaps adds neons to their PC.&lt;/p&gt;
&lt;p&gt;Strengths: Cool. Elegant. Reliable.&lt;/p&gt;
&lt;h3&gt;Harvard&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/harvard.png&quot; alt=&quot;Harvard status page with background header image of yard. The status of various services is shown, along with various links.&quot; /&gt;
&lt;em&gt;Harvard&apos;s status page has a friendly look to it&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://status.huit.harvard.edu/&quot;&gt;Harvard&lt;/a&gt; is one of the world&apos;s most renowned educational institutions. Its status page has a pastoral quality. It uses a subtle green that nods to the header image of the Harvard Yard. In addition to showing the status of various services, it provides useful links that help students easily solve their problems.&lt;/p&gt;
&lt;p&gt;Strengths: Calm. Reassuring. Pastoral.&lt;/p&gt;
&lt;h3&gt;Graphite&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/graphite.png&quot; alt=&quot;Graphite status page with displays showing service history and service status for a selection of services, including Slack status and Github status&quot; /&gt;
&lt;em&gt;Graphite&apos;s status page looks cool, but not at the expense of usability&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://status.graphite.com/&quot;&gt;Graphite&lt;/a&gt; is an AI code review platform pitched at developers. Its status page uses green and oozes cold technical sophistication. Its mouseover popups provide additional details, and there&apos;s a clear, readable incident log. The only downside is the mildly confusing empty pop-ups on incident-free days.&lt;/p&gt;
&lt;p&gt;Strengths: Slick. Clear. Classy.&lt;/p&gt;
&lt;h3&gt;PagerTree&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/pagertree.png&quot; alt=&quot;PagerTree status page showing list of services with data for each one, such as response time and uptime.&quot; /&gt;
&lt;em&gt;PagerTree&apos;s status page is easy to understand, but contains plenty of useful information&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://status.pagertree.com/&quot;&gt;PagerTree&lt;/a&gt; is a management system for on-call teams. Its bold, clear page includes a brief list of services, with response times and uptime listed in each row. As well as being easy to understand, it includes technical data, and you can click on each day to see more.&lt;/p&gt;
&lt;p&gt;Strengths: Bold. Organized. Informative.&lt;/p&gt;
&lt;h3&gt;Docker&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/docker.png&quot; alt=&quot;Docker status page, with response time and uptime displays.&quot; /&gt;
&lt;em&gt;Docker&#x2019;s status page provides graphs of uptime and response time&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dockerstatus.com/&quot;&gt;Docker&lt;/a&gt; is a containerization system that makes deploying software easier and more secure. Its status page is efficient and informative, with details of each service available on mouseover and plenty of metrics below. Docker users are likely to be technical and to pay close attention to performance, reflected in the selection of response-time metrics alongside the more common uptimes on display.&lt;/p&gt;
&lt;p&gt;Strengths: Metrics. Efficiency. Detailed.&lt;/p&gt;
&lt;h3&gt;Vimeo&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/vimeo.png&quot; alt=&quot;Vimeo status page with table showing ticks next to its various services. Also includes a header and a key at the bottom.&quot; /&gt;
&lt;em&gt;Vimeo&apos;s well-organized page fits all its services onto a single screen&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.vimeostatus.com/&quot;&gt;Vimeo&lt;/a&gt; is a professionally oriented video platform. Its status page shows off its professionalism, with simple ticks indicating that services are working. It also manages to get all its services onto a single page, with a clear incident log underneath. A few links at the top provide additional information.&lt;/p&gt;
&lt;p&gt;Strengths: Orderly. Organized. Compact.&lt;/p&gt;
&lt;h3&gt;RubyGems&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/rubygems.png&quot; alt=&quot;Last in the list of great status page examples. RubyGems page showing uptime of its various services and incident history.&quot; /&gt;
&lt;em&gt;RubyGems uptime page is clear, but packs all the important details&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://uptime.rubygems.org/&quot;&gt;RubyGems&lt;/a&gt; lets Ruby users and developers share code packages. Its uptime status page is simple, clean, and easy to understand. Delivered by Honeybadger, it shows both response times and uptime and makes it clear what has been happening recently, which in this case is nothing bad. Its three main services are each displayed on a single line, with their metrics clearly labeled.&lt;/p&gt;
&lt;p&gt;As well as being clear enough to read at a glance, it also packs details into its design that you can see without having to scroll or hunt around to find. You can click on individual days to view more detailed metrics, too.&lt;/p&gt;
&lt;p&gt;Strengths: Tidy. Detailed. Powerful.&lt;/p&gt;
&lt;h2&gt;Issues to look out for when creating a status page&lt;/h2&gt;
&lt;p&gt;When you&#x2019;re creating a status page, there are a few key things to consider.&lt;/p&gt;
&lt;h3&gt;Is your status page easy to manage?&lt;/h3&gt;
&lt;p&gt;You want the status page to take care of itself once it&apos;s up. Ideally, it should be able to grab data automatically. It should handle outages across any of its data sources and show that the services have been restored when they come back online.&lt;/p&gt;
&lt;p&gt;It should be able to dynamically adjust the display period if that&#x2019;s relevant, perhaps highlighting longer periods of uptime. If not, it should be easy for you to change&#x2014;and that goes for all changes you want to make.&lt;/p&gt;
&lt;p&gt;If you have an incident log, it should be easy to update, preferably by pulling data directly from your logging system, with an easy way to edit it if you need to provide users with more specific details.&lt;/p&gt;
&lt;h3&gt;Does your status page integrate well with monitoring tools?&lt;/h3&gt;
&lt;p&gt;Status pages need data; the more the better. Wiring it all up manually is one approach, but it&apos;s slow and expensive. Automatically piping it in is faster and more efficient. Honeybadger gives you a simple set of tools to do that, with key features such as uptime checking and cron job monitoring, so you can use it to track services that might otherwise be tricky to monitor.&lt;/p&gt;
&lt;p&gt;Incident management features are great to have too, allowing you to report to your users, provide a comprehensive overview of your service status, and significantly enhance the customer experience. Atlassian Statuspage has these, as does Honeybadger.&lt;/p&gt;
&lt;h3&gt;Is your status page showing users what they want?&lt;/h3&gt;
&lt;p&gt;Simplicity is key here. Most of the pages listed above are not flashy. Why? Customers are in a rush&#x2014;possibly even a panic&#x2014;when they struggle to connect to a service. They don&#x2019;t want to be distracted. They need to know what&#x2019;s going on, quickly.&lt;/p&gt;
&lt;p&gt;Visually appealing status pages are fantastic, but utility should always be the priority. Make sure your page aligns with what your users want and need (which aren&#x2019;t always the same thing). This requires some empathy. Gathering quality feedback can help, and providing a quick way for users to contact you on your status page may also help you understand their needs.&lt;/p&gt;
&lt;h2&gt;How to create the best status page in Honeybadger&lt;/h2&gt;
&lt;p&gt;Now that we know what makes the &lt;a href=&quot;https://www.honeybadger.io/tour/status-pages/&quot;&gt;best status pages&lt;/a&gt; great, it&apos;s time to set up our own. Fortunately, Honeybadger makes that very easy. It provides hosted status pages that include several advanced features.&lt;/p&gt;
&lt;p&gt;With it, you can create a status page in just a few clicks. With a little more work, you can also integrate with its monitoring tools, letting you check your uptime or monitor cron jobs, for example. Honeybadger also has incident management features, letting you inform users about recent events. As well as a website, you can use a Honeybadger status page to monitor your apps, APIs, or other services.&lt;/p&gt;
&lt;p&gt;To begin, you first need a Honeybadger account, so go to &lt;a href=&quot;https://www.honeybadger.io/&quot;&gt;Honeybadger&#x2019;s homepage&lt;/a&gt; and create one if you haven&#x2019;t already.&lt;/p&gt;
&lt;p&gt;Next, navigate to &lt;a href=&quot;https://app.honeybadger.io/status_pages&quot;&gt;Honeybadger&apos;s status page management screen&lt;/a&gt;, then click the &#x201c;Create your first status page&#x201d; button. You could also check out the &lt;a href=&quot;https://docs.honeybadger.io/guides/uptime/&quot;&gt;guide to uptime monitoring&lt;/a&gt; if you want to learn more.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/create-your-own-status-page.png&quot; alt=&quot;Honeybadger page with a button to build your own status page.&quot; /&gt;
&lt;em&gt;Clicking this button is the first step to getting your own page set up&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That will take you to the new status page screen. Give your page a name and fill in the details as needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/new-status-page.png&quot; alt=&quot;Honeybadger&#x2019;s create status screen page.&quot; /&gt;
&lt;em&gt;The new status page screen is nice and simple, but also contains powerful extras&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You can also display a message to users of your application or website, connect Google Analytics, and more. Business accounts can add password protection and extra customizations.&lt;/p&gt;
&lt;p&gt;When you&apos;re ready, click &#x201c;Create Page,&#x201d; and voila, your page will be created. It could hardly be more user-friendly. It follows a basic status page template. There are controls that let you edit or delete your page, create incidents, view ongoing incidents, and view a log of historical data.&lt;/p&gt;
&lt;p&gt;Here&apos;s the page in all its glory.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/status-page-examples/first-status-page.png&quot; alt=&quot;Our Honeybadger status page. Shows a list of months with no incidents next to them.&quot; /&gt;
&lt;em&gt;Here&#x2019;s a newly created Honeybadger status page&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is just a taste of what you can do. From here, you can add a logo, including a separate one for dark mode. You can add a favicon too, so it matches your site. You can also attach your own custom domain. Take a look through the &lt;a href=&quot;https://docs.honeybadger.io/guides/status-pages/&quot;&gt;Honeybadger documentation&lt;/a&gt; to learn what else is available.&lt;/p&gt;
&lt;h2&gt;Creating a status page is easier than you think with Honeybadger&lt;/h2&gt;
&lt;p&gt;A status page is a key part of your product offering, helping you keep users informed and building trust with customers. Building one shouldn&#x2019;t be an afterthought. Take care to build one that delivers everything your clients want and shows your ability to deliver a consistently reliable product.&lt;/p&gt;
&lt;p&gt;If these status page examples have inspired you, it&apos;s time to start working on your own. Fortunately, you can have a well-designed status page ready within minutes. Sign up for a &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;free trial of Honeybadger&lt;/a&gt; and see how easy it is to build your own.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Next.js error handling: a practical guide</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/next-js-error-handling/"/>
    <id>https://www.honeybadger.io/blog/next-js-error-handling/</id>
    <published>2026-06-18T07:00:00+00:00</published>
    <updated>2026-06-18T07:00:00+00:00</updated>
    <author>
      <name>Farhan Hasin Chowdhury</name>
    </author>
    <summary type="text">Next.js gives developers a structured way to handle errors at every level of an application &#x2014; from form validation to root-level crashes. Learn how to manage expected errors with return values, catch uncaught exceptions with error boundaries, and set up automatic error reporting in production.</summary>
    <content type="html">&lt;p&gt;Every Next.js application has to deal with errors, whether it&apos;s a failed API call, invalid user input, or a bug that slips into production. The App Router gives you built-in tools to handle each of these cases differently, keeping your UI intact and your users informed.&lt;/p&gt;
&lt;p&gt;No matter how carefully you write your code, things will go wrong. An API will go down, a user will submit unexpected input, or a bug will surface in production. Next.js error handling, especially with the App Router, gives you a structured way to deal with both the errors you expect and the ones that catch you off guard. In this article, I&apos;ll walk you through how to handle errors at every level of a Next.js application and show you how to integrate Honeybadger so that no error goes unnoticed.&lt;/p&gt;
&lt;h2&gt;Handling expected errors&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/next-js-error-handling/error-handling-flow.png&quot; alt=&quot;Flowchart showing the Next.js error handling decision tree, from expected errors handled via return values to uncaught exceptions caught by error boundaries, with Honeybadger reporting at the end&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Expected errors are the ones you can anticipate: a user submits a form without filling in the required fields, an API request returns a 404, or an authentication check fails. These aren&apos;t bugs. They&apos;re normal outcomes of how web applications work. The key principle in Next.js is to &lt;strong&gt;model expected errors as return values, not thrown exceptions&lt;/strong&gt;. You return a value that describes what went wrong and let the UI respond accordingly.&lt;/p&gt;
&lt;h3&gt;Server Actions&lt;/h3&gt;
&lt;p&gt;Let&apos;s start with Server Actions. Imagine you have a form that lets users create a new blog post. The Server Action needs to validate the input and communicate with an API, and either step could fail:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/actions.ts
&apos;use server&apos;

export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get(&apos;title&apos;)

  if (!title || typeof title !== &apos;string&apos; || title.trim().length === 0) {
    return { message: &apos;Title is required.&apos; }
  }

  const res = await fetch(&apos;https://api.example.com/posts&apos;, {
    method: &apos;POST&apos;,
    headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
    body: JSON.stringify({ title: title.trim() }),
  })

  if (!res.ok) {
    return { message: &apos;Failed to create post. Please try again.&apos; }
  }

  // If we get here, everything worked
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of throwing an error, the function returns an object with a &lt;code&gt;message&lt;/code&gt; property. On the client, you consume this with React&apos;s &lt;code&gt;useActionState&lt;/code&gt; hook:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&apos;use client&apos;

import { useActionState } from &apos;react&apos;
import { createPost } from &apos;./actions&apos;

export function PostForm() {
  const [state, formAction, pending] = useActionState(createPost, { message: &apos;&apos; })

  return (
    &amp;lt;form action={formAction}&amp;gt;
      &amp;lt;label htmlFor=&amp;quot;title&amp;quot;&amp;gt;Post Title&amp;lt;/label&amp;gt;
      &amp;lt;input id=&amp;quot;title&amp;quot; type=&amp;quot;text&amp;quot; name=&amp;quot;title&amp;quot; required /&amp;gt;
      &amp;lt;button type=&amp;quot;submit&amp;quot; disabled={pending}&amp;gt;
        {pending ? &apos;Creating...&apos; : &apos;Create Post&apos;}
      &amp;lt;/button&amp;gt;
      {state?.message &amp;amp;&amp;amp; (
        &amp;lt;p role=&amp;quot;alert&amp;quot; className=&amp;quot;error&amp;quot;&amp;gt;{state.message}&amp;lt;/p&amp;gt;
      )}
    &amp;lt;/form&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hook gives you the current state (including the error message), the form action, and a pending boolean. When the Server Action returns an error, the component re-renders and the error message appears below the form. The user sees exactly what went wrong and can try again.&lt;/p&gt;
&lt;h3&gt;Server Components&lt;/h3&gt;
&lt;p&gt;In Server Components, you can check whether a request succeeded and conditionally render different UI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default async function PostPage({ params }: { params: Promise&amp;lt;{ id: string }&amp;gt; }) {
  const { id } = await params
  const res = await fetch(`https://api.example.com/posts/${id}`)

  if (!res.ok) {
    return (
      &amp;lt;div className=&amp;quot;error-state&amp;quot;&amp;gt;
        &amp;lt;h2&amp;gt;Could not load this post&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;The server returned an error. Please try again later.&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    )
  }

  const post = await res.json()

  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{post.content}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You fetch data, check the response, and if it&apos;s not ok, you return a fallback UI instead of the normal content. This same pattern works when you fetch data in any Server Component.&lt;/p&gt;
&lt;h3&gt;Handling 404s with &lt;code&gt;notFound()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Next.js provides a dedicated &lt;code&gt;notFound()&lt;/code&gt; function from &lt;code&gt;next/navigation&lt;/code&gt; for the 404 case. When you call it, Next.js stops rendering the current page and shows a 404 UI instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { notFound } from &apos;next/navigation&apos;

export default async function PostPage({ params }: { params: Promise&amp;lt;{ id: string }&amp;gt; }) {
  const { id } = await params
  const post = await getPost(id)

  if (!post) {
    notFound()
  }

  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{post.content}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, Next.js renders a generic 404 error page when &lt;code&gt;notFound()&lt;/code&gt; is called. But you can create a custom error page by adding a &lt;code&gt;not-found.tsx&lt;/code&gt; file in the same route segment. For example, if you place a &lt;code&gt;not-found.tsx&lt;/code&gt; file inside &lt;code&gt;app/blog/&lt;/code&gt;, it will render your custom error page whenever &lt;code&gt;notFound()&lt;/code&gt; is called from any page within that route segment.&lt;/p&gt;
&lt;h2&gt;Handling uncaught exceptions&lt;/h2&gt;
&lt;p&gt;The other type of error is the ones you didn&apos;t see coming &#x2014; a null reference, a third-party library throwing unexpectedly, or a network request failing in a way you didn&apos;t account for. These are actual bugs. Next.js handles them with &lt;strong&gt;error boundaries&lt;/strong&gt;, a React concept where a component catches errors during rendering and displays a fallback UI instead of crashing the entire component tree. The App Router builds this directly into the file system routing convention.&lt;/p&gt;
&lt;h3&gt;The error.tsx convention&lt;/h3&gt;
&lt;p&gt;Create a file called &lt;code&gt;error.tsx&lt;/code&gt; in any route segment. It must be a Client Component and receives two props: the &lt;code&gt;error&lt;/code&gt; object and an &lt;code&gt;unstable_retry&lt;/code&gt; function that lets the user try again (the &lt;code&gt;unstable_&lt;/code&gt; prefix means this API may change in a future release):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/error.tsx
&apos;use client&apos;

export default function DashboardError({
  error,
  unstable_retry,
}: {
  error: Error &amp;amp; { digest?: string }
  unstable_retry: () =&amp;gt; void
}) {
  return (
    &amp;lt;div className=&amp;quot;error-container&amp;quot;&amp;gt;
      &amp;lt;h2&amp;gt;Something went wrong&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;An unexpected error occurred while loading the dashboard.&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; unstable_retry()}&amp;gt;Try again&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When an uncaught error occurs anywhere within the &lt;code&gt;app/dashboard/&lt;/code&gt; route segment or its children, Next.js will catch the error and render this fallback UI instead of crashing the page. The &lt;code&gt;unstable_retry&lt;/code&gt; function re-renders the segment without a full page reload, which is handy for transient errors like network timeouts.&lt;/p&gt;
&lt;p&gt;Note the &lt;code&gt;digest&lt;/code&gt; property on the error object. When a server-side error occurs, Next.js replaces the original error message with a hash to avoid leaking sensitive details like database queries or file paths. The full error message stays in your server-side logs.&lt;/p&gt;
&lt;h3&gt;Nested error boundaries&lt;/h3&gt;
&lt;p&gt;Errors bubble upward through the route hierarchy until they hit the nearest error boundary, so you can be strategic about where you place your &lt;code&gt;error.tsx&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/next-js-error-handling/error-boundary-bubbling.png&quot; alt=&quot;Diagram showing how errors bubble upward through Next.js error boundaries, from page components to the nearest error.tsx and ultimately to global-error.tsx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For example, imagine a dashboard with a sidebar and a main content area. By placing &lt;code&gt;error.tsx&lt;/code&gt; at the right level, you can contain a crash to just the affected area while keeping the sidebar functional:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;app/
  dashboard/
    error.tsx          # Catches errors from all dashboard child routes
    layout.tsx         # Dashboard layout with sidebar (stays intact)
    page.tsx           # Main dashboard page
    analytics/
      error.tsx        # Catches errors only in the analytics section
      page.tsx
    settings/
      page.tsx         # Errors here bubble up to dashboard/error.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the analytics page crashes, only that section shows the error UI &#x2014; the sidebar stays intact. If settings crashes, the error bubbles up to &lt;code&gt;dashboard/error.tsx&lt;/code&gt; since it doesn&apos;t have its own error boundary.&lt;/p&gt;
&lt;h3&gt;global-error.tsx&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;error.tsx&lt;/code&gt; can&apos;t catch errors in the root layout since it wraps everything. For that, Next.js provides &lt;code&gt;app/global-error.tsx&lt;/code&gt;, which replaces the &lt;strong&gt;entire page&lt;/strong&gt; when triggered and must define its own &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tags:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/global-error.tsx
&apos;use client&apos;

export default function GlobalError({
  error,
  unstable_retry,
}: {
  error: Error &amp;amp; { digest?: string }
  unstable_retry: () =&amp;gt; void
}) {
  return (
    &amp;lt;html&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;div className=&amp;quot;global-error&amp;quot;&amp;gt;
          &amp;lt;h2&amp;gt;Something went wrong&amp;lt;/h2&amp;gt;
          &amp;lt;p&amp;gt;We encountered an unexpected error. Please try refreshing the page.&amp;lt;/p&amp;gt;
          &amp;lt;button onClick={() =&amp;gt; unstable_retry()}&amp;gt;Try again&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In practice, errors in the root layout are rare, but having a global error page in place ensures your users never see a completely broken page.&lt;/p&gt;
&lt;h3&gt;Event handler errors&lt;/h3&gt;
&lt;p&gt;There&apos;s an important limitation to be aware of: error boundaries only catch errors that occur &lt;strong&gt;during rendering&lt;/strong&gt;. If an error happens inside an event handler, like an &lt;code&gt;onClick&lt;/code&gt; or &lt;code&gt;onSubmit&lt;/code&gt; callback, the nearest error boundary won&apos;t catch it because event handlers run outside of React&apos;s rendering cycle.&lt;/p&gt;
&lt;p&gt;For these cases, you need to catch errors manually with a try/catch block:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&apos;use client&apos;

import { useState } from &apos;react&apos;

export function DeleteButton({ id }: { id: string }) {
  const [error, setError] = useState&amp;lt;string | null&amp;gt;(null)

  const handleDelete = async () =&amp;gt; {
    try {
      const res = await fetch(`/api/posts/${id}`, { method: &apos;DELETE&apos; })
      if (!res.ok) {
        setError(&apos;Failed to delete this post. Please try again.&apos;)
      }
    } catch (e) {
      setError(&apos;Something went wrong. Please check your connection.&apos;)
    }
  }

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={handleDelete}&amp;gt;Delete&amp;lt;/button&amp;gt;
      {error &amp;amp;&amp;amp; &amp;lt;p role=&amp;quot;alert&amp;quot; className=&amp;quot;error&amp;quot;&amp;gt;{error}&amp;lt;/p&amp;gt;}
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a common pattern in client-side error handling. One exception: if you use &lt;code&gt;useTransition&lt;/code&gt; and throw an error inside &lt;code&gt;startTransition&lt;/code&gt;, that error &lt;em&gt;will&lt;/em&gt; bubble up to the nearest error boundary, even from an event handler.&lt;/p&gt;
&lt;h2&gt;Capturing and reporting Next.js errors with Honeybadger&lt;/h2&gt;
&lt;p&gt;Proper error handling in the UI is one thing, but in production, you also need to know when errors are happening, how often, and what&apos;s causing them. Users almost never report bugs, and digging through server-side logs is tedious at best.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.honeybadger.io/lib/javascript/integration/nextjs/&quot;&gt;Honeybadger&apos;s Next.js integration&lt;/a&gt; plugs into your application to automatically catch errors on both the server side and client side, group duplicates, and notify you with enough context to actually fix things.&lt;/p&gt;
&lt;h3&gt;Installation and setup&lt;/h3&gt;
&lt;p&gt;First, install the required packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @honeybadger-io/react @honeybadger-io/nextjs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run the setup command to generate the configuration files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx honeybadger-copy-config-files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates configuration files for the server, client, and edge runtimes, as well as &lt;code&gt;error.tsx&lt;/code&gt; and &lt;code&gt;global-error.tsx&lt;/code&gt; files for the App Router. Next, add your API key to your environment variables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;NEXT_PUBLIC_HONEYBADGER_API_KEY=your_api_key
NEXT_PUBLIC_HONEYBADGER_REVISION=your_deployment_revision
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, wrap your Next.js config with Honeybadger&apos;s setup function to enable source map uploads and error reporting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// next.config.js
const { setupHoneybadger } = require(&apos;@honeybadger-io/nextjs&apos;)

const nextConfig = {}

module.exports = setupHoneybadger(nextConfig)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Source maps are uploaded automatically, so stack traces in Honeybadger point to your original source code instead of minified bundles.&lt;/p&gt;
&lt;h3&gt;Wrapping your app with the error boundary&lt;/h3&gt;
&lt;p&gt;Wrap your application with Honeybadger&apos;s error boundary in your root layout so any unhandled React component error is automatically reported:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/layout.tsx
import { Honeybadger, HoneybadgerErrorBoundary } from &apos;@honeybadger-io/react&apos;

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    &amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;HoneybadgerErrorBoundary honeybadger={Honeybadger}&amp;gt;
          {children}
        &amp;lt;/HoneybadgerErrorBoundary&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honeybadger will now capture uncaught client-side exceptions and React component errors. You can also add the &lt;code&gt;showUserFeedbackFormOnError&lt;/code&gt; prop to show a feedback form when an error occurs, letting users describe what they were doing.&lt;/p&gt;
&lt;h3&gt;Manual error reporting&lt;/h3&gt;
&lt;p&gt;For errors you handle gracefully but still want to track, use &lt;code&gt;Honeybadger.notify()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Honeybadger } from &apos;@honeybadger-io/react&apos;

try {
  await submitOrder(orderData)
} catch (error) {
  Honeybadger.notify(error)
  return { message: &apos;Order failed. Please try again.&apos; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also attach context to help with debugging:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;Honeybadger.setContext({
  user_id: currentUser.id,
  user_email: currentUser.email,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When an error shows up in Honeybadger&apos;s dashboard, you&apos;ll know exactly which user was affected and have a full stack trace pointing to the exact line of code.&lt;/p&gt;
&lt;h2&gt;Next.js error handling best practices&lt;/h2&gt;
&lt;p&gt;To recap, here&apos;s how I think about Next.js error handling.&lt;/p&gt;
&lt;p&gt;For expected errors, return values instead of throwing. Use &lt;code&gt;useActionState&lt;/code&gt; in Server Actions and conditional rendering in Server Components to handle errors gracefully. Use &lt;code&gt;notFound()&lt;/code&gt; for missing resources instead of rolling your own 404 logic.&lt;/p&gt;
&lt;p&gt;For uncaught exceptions, place &lt;code&gt;error.tsx&lt;/code&gt; files at the right levels of your route hierarchy so a crash in one section doesn&apos;t take down the whole page. Add &lt;code&gt;global-error.tsx&lt;/code&gt; as a safety net for root layout errors. And remember that error boundaries don&apos;t catch errors in event handlers, so you&apos;ll need try/catch there.&lt;/p&gt;
&lt;p&gt;Finally, don&apos;t rely on users to tell you when things break. Set up error monitoring so you find out about production issues before your users do.&lt;/p&gt;
&lt;p&gt;That covers the full picture of error handling in Next.js, from data fetching and form validation to production error management. If you want to try Honeybadger with your Next.js project, you can &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;sign up for a free trial&lt;/a&gt; and have error reporting running in a few minutes.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Next.js vs React: What&#x2019;s the difference and which should you use?</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/next-js-vs-react/"/>
    <id>https://www.honeybadger.io/blog/next-js-vs-react/</id>
    <published>2026-05-28T07:00:00+00:00</published>
    <updated>2026-05-28T07:00:00+00:00</updated>
    <author>
      <name>Muhammed Ali</name>
    </author>
    <summary type="text">Next.js and React are often compared, but they solve different problems. React focuses on building user interfaces, while Next.js adds structure, rendering strategies, and back-end capabilities. Read this article to see how we break down their differences.</summary>
    <content type="html">&lt;p&gt;The Next.js vs React question is not really a comparison between two competing tools &#x2014; Next.js is built on top of React. React itself is a UI rendering JavaScript library used for building user interfaces across platforms, including web applications and mobile apps with React Native, while Next.js is a framework that wraps React and makes concrete decisions about routing, data fetching, and server-side concerns. Understanding this relationship is the starting point for every project decision you will make when building web applications.&lt;/p&gt;
&lt;p&gt;React handles one job extremely well: taking a component tree and turning it into DOM output, then reconciling changes efficiently. Every other layer like how you fetch data, how you route between pages, what runs on the server versus the client is deliberately left to the developer or to third-party libraries. Next.js packages those decisions into a cohesive framework, adding server-side rendering, file-system-based routing, built-in image optimization, and an API routing layer that runs alongside your JavaScript code. This makes it particularly useful for complex projects that require coordination between the front-end and back-end.&lt;/p&gt;
&lt;p&gt;This article covers what each tool does at a technical level, how they differ in behavior and file structure, how their rendering modes work, and answers the common question: what is Next.js vs React in practical terms?&lt;/p&gt;
&lt;h2&gt;What is React?&lt;/h2&gt;
&lt;p&gt;React&apos;s core contribution to web development is the component-based architecture combined with a virtual DOM diffing algorithm. Before React, updating the DOM in response to state changes meant either re-rendering entire page sections or writing granular imperative update logic that quickly became unmaintainable.&lt;/p&gt;
&lt;p&gt;React introduced a declarative model:&#xa0;describe what the user interface should look like for a given state, and let the reconciler determine the minimum set of real DOM mutations required. This approach transformed web development workflows and made React a widely adopted JavaScript library for building complex user interfaces.&lt;/p&gt;
&lt;h3&gt;The component and hook model&lt;/h3&gt;
&lt;p&gt;A React component is a function that accepts props and returns JSX. The function re-runs whenever its state or props change, and React reconciles the output against the previous virtual DOM snapshot. Hooks like &lt;code&gt;useState&lt;/code&gt; and &lt;code&gt;useEffect&lt;/code&gt; let you attach stateful behavior and side effects to functional components without resorting to class syntax.&lt;/p&gt;
&lt;p&gt;In practice, components are composed hierarchically, where child components receive data and callbacks from their parents. This composition model is what enables React to scale cleanly in complex projects, especially when managing deeply nested user interface trees.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/components/UserCard.tsx

import { useState, useEffect } from &apos;react&apos;;

interface User {
  id: number;
  name: string;
  email: string;
}
interface UserCardProps {
  userId: number;
}

export function UserCard({ userId }: UserCardProps) {
  const [user, setUser] = useState&amp;lt;User | null&amp;gt;(null);
  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    fetch(`/api/users/${userId}`)
      .then((res) =&amp;gt; res.json())
      .then((data) =&amp;gt; {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;;
  if (!user) return &amp;lt;div&amp;gt;User not found&amp;lt;/div&amp;gt;;

  return (
    &amp;lt;div className=&amp;quot;card&amp;quot;&amp;gt;
      &amp;lt;h2&amp;gt;{user.name}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{user.email}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This component fetches a user when the &lt;code&gt;userId&lt;/code&gt; prop changes, tracks the loading state, and renders conditionally based on that state. The &lt;code&gt;useEffect&lt;/code&gt; dependency array &lt;code&gt;[userId]&lt;/code&gt; ensures the fetch only re-runs when &lt;code&gt;userId&lt;/code&gt; changes, not on every render. This pattern is idiomatic React, but notice that the data fetch happens entirely in the browser after the component mounts. There is no concept of running this on a server.&lt;/p&gt;
&lt;h3&gt;What React deliberately leaves out&lt;/h3&gt;
&lt;p&gt;React provides no routing system. Navigation between views requires installing a library like React Router or TanStack Router. React also has no built-in data fetching convention, no server-side rendering pipeline, no image optimization, and no API routes server. You can combine React with Express for server-side rendering and with third-party libraries like SWR or TanStack Query for caching, but you must assemble these pieces yourself across multiple JavaScript files.&lt;/p&gt;
&lt;p&gt;This is not a weakness. For applications that run entirely in the browser, including dashboards and progressive web apps, the absence of framework opinions means less configuration overhead and more flexibility in your dependency choices. The cost is that you own the architecture decisions.&lt;/p&gt;
&lt;p&gt;React also benefits from a large and active community, which means most problems you encounter already have established patterns or libraries.&lt;/p&gt;
&lt;h2&gt;What is Next.js and how does it extend React?&lt;/h2&gt;
&lt;p&gt;Next.js extends the React component model with conventions and runtime capabilities that React itself does not provide. The two most significant additions are the rendering pipeline (Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR)) and the file-based routing system. Everything else&#x2014;like API handling, image optimization, the Edge Runtime, font loading, and middleware&#x2014;builds on top of those two foundations.&lt;/p&gt;
&lt;h3&gt;The App Router and Server Components&lt;/h3&gt;
&lt;p&gt;Next.js 13 introduced the App Router, which changed the default rendering model. In the App Router, every component is a React Server Component (RSC) unless you explicitly opt out with the &lt;code&gt;&apos;use client&apos;&lt;/code&gt; directive.&lt;/p&gt;
&lt;p&gt;Server Components render on the server, can &lt;a href=&quot;https://www.honeybadger.io/blog/javascript-concurrency/&quot;&gt;await async operations&lt;/a&gt; directly in the component body, and never ship their JavaScript code or the data-fetching JavaScript libraries they use to the client bundle. This results in automatic code splitting, where only the required interactive parts of the application are sent to the browser.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/users/[id]/page.tsx - runs on the server, zero client JS

interface PageProps {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
}

async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`, {
    next: { revalidate: 60 }, // ISR: revalidate this data every 60 seconds
  });
  if (!res.ok) throw new Error(&apos;Failed to fetch user&apos;);
  return res.json();
}

export default async function UserPage({ params }: PageProps) {
  const { id } = await params;
  const user = await getUser(id);
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;{user.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{user.email}&amp;lt;/p&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fundamentally different from the React example above. The component is declared &lt;code&gt;async&lt;/code&gt; and awaits the data directly, without &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useState&lt;/code&gt;, or state management libraries. The fetch happens at request time on the server. The client receives pre-rendered HTML. The &lt;code&gt;next: { revalidate: 60 }&lt;/code&gt; option activates Incremental Static Regeneration, so after the initial render, Next.js regenerates the page in the background when a request arrives after 60 seconds, serving the stale version until the fresh one is ready.&lt;/p&gt;
&lt;h3&gt;Client Components and the &apos;use client&apos; boundary&lt;/h3&gt;
&lt;p&gt;When a component needs interactivity, you add &lt;code&gt;&apos;use client&apos;&lt;/code&gt; at the top of the file. This converts the component to a Client Component, which is hydrated in the browser. The boundary between Server and Client Components is explicit and composable: a Server Component can render interactive child components, but a Client Component cannot render a Server Component directly. This separation enables granular automatic code splitting and reduces bundle size.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/components/AddToCartButton.tsx
&apos;use client&apos;;

import { useState } from &apos;react&apos;;

interface AddToCartButtonProps {
  productId: string;
  price: number;
}

export function AddToCartButton({ productId, price }: AddToCartButtonProps) {
  const [added, setAdded] = useState(false);

  async function handleClick() {
    await fetch(&apos;/api/cart&apos;, {
      method: &apos;POST&apos;,
      body: JSON.stringify({ productId, quantity: 1 }),
      headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
    });
    setAdded(true);
  }

  return (
    &amp;lt;button onClick={handleClick} disabled={added}&amp;gt;
      {added ? &apos;Added to cart&apos; : `Add to cart- $${price}`}
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The product page itself (a Server Component) fetches product data on the server and renders HTML, while interactive child components handle user interactions like adding items to a cart. This pattern keeps user interfaces fast and lightweight. This granular control over the client bundle size is one of the most architecturally significant advantages Next.js provides over a pure React SPA (Single Page Application).&lt;/p&gt;
&lt;h2&gt;Key differences&lt;/h2&gt;
&lt;p&gt;The divergence between Next.js and a standalone React application becomes concrete when you compare how each handles routing, the rendering pipeline, project layout, search engine visibility, and runtime performance. These are not surface-level configuration differences; they reflect fundamentally different execution models.&lt;/p&gt;
&lt;h3&gt;Routing&lt;/h3&gt;
&lt;p&gt;React has no router. You install React Router and configure routes manually across multiple JavaScript files. Next.js uses file-based routing. Creating a file automatically registers a route. This removes boilerplate and makes it easier to scale routing in complex projects.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/main.tsx - React + React Router v6

import { createBrowserRouter, RouterProvider } from &apos;react-router-dom&apos;;
import { RootLayout } from &apos;./layouts/RootLayout&apos;;
import { HomePage } from &apos;./pages/HomePage&apos;;
import { ProductPage } from &apos;./pages/ProductPage&apos;;
import { NotFound } from &apos;./pages/NotFound&apos;;
import ReactDOM from &apos;react-dom/client&apos;;

const router = createBrowserRouter([
  {
    path: &apos;/&apos;,
    element: &amp;lt;RootLayout /&amp;gt;,
    errorElement: &amp;lt;NotFound /&amp;gt;,
    children: [
      { index: true, element: &amp;lt;HomePage /&amp;gt; },
      { path: &apos;products/:id&apos;, element: &amp;lt;ProductPage /&amp;gt; },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById(&apos;root&apos;)!).render(
  &amp;lt;RouterProvider router={router} /&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next.js uses a file-based routing system. Creating a file at &lt;code&gt;app/products/[id]/page.tsx&lt;/code&gt; automatically registers the route &lt;code&gt;/products/:id&lt;/code&gt;. The folder structure is the route definition. Layouts, loading states, error boundaries, and not-found pages are handled by special files (&lt;code&gt;layout.tsx&lt;/code&gt;, &lt;code&gt;loading.tsx&lt;/code&gt;, &lt;code&gt;error.tsx&lt;/code&gt;, &lt;code&gt;not-found.tsx&lt;/code&gt;) placed in the corresponding route segment directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;app/
&#x251c;&#x2500;&#x2500; layout.tsx          &#x2192; root layout, wraps all routes
&#x251c;&#x2500;&#x2500; page.tsx            &#x2192; renders at /
&#x251c;&#x2500;&#x2500; loading.tsx         &#x2192; Suspense fallback for /
&#x251c;&#x2500;&#x2500; error.tsx           &#x2192; error boundary for /
&#x251c;&#x2500;&#x2500; products/
&#x2502;   &#x251c;&#x2500;&#x2500; page.tsx        &#x2192; renders at /products
&#x2502;   &#x2514;&#x2500;&#x2500; [id]/
&#x2502;       &#x251c;&#x2500;&#x2500; page.tsx    &#x2192; renders at /products/:id
&#x2502;       &#x2514;&#x2500;&#x2500; loading.tsx &#x2192; Suspense fallback for /products/:id
&#x2514;&#x2500;&#x2500; api/
    &#x2514;&#x2500;&#x2500; cart/
        &#x2514;&#x2500;&#x2500; route.ts    &#x2192; API endpoint at /api/cart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file-system router removes an entire category of configuration work. You do not write route declarations, import pages manually, or manage a separate router configuration object. The trade-off is that your folder structure becomes load-bearing.&lt;/p&gt;
&lt;h3&gt;Rendering modes&lt;/h3&gt;
&lt;p&gt;A plain React application renders entirely in the browser. The server sends a mostly empty HTML document with a script tag, and React bootstraps the application in the client. This is client-side rendering (CSR). Next.js supports multiple rendering strategies and enables automatic code splitting at the route and component level. This significantly improves performance for large web applications. Search engines and users on slow connections both receive no meaningful content until the JavaScript parses, executes, and renders.&lt;/p&gt;
&lt;p&gt;Next.js supports four rendering strategies, and you can mix them within a single application. Static site generation renders pages at build time, the HTML is computed once and served as a static file on every request.&lt;/p&gt;
&lt;p&gt;Server-side rendering (SSR) renders on each request, allowing you to fetch fresh data per user or session. Incremental Static Regeneration (ISR) generates the page statically but revalidates it on a configurable interval. Client-side rendering is available for components marked with &lt;code&gt;&apos;use client&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/blog/[slug]/page.tsx

// Generate static paths at build time (static site generation)
export async function generateStaticParams() {
  const posts = await fetch(&apos;https://api.example.com/posts&apos;).then(r =&amp;gt; r.json());
  return posts.map((post: { slug: string }) =&amp;gt; ({ slug: post.slug }));
}

// Revalidate every 10 minutes (ISR)
export const revalidate = 600;

export default async function BlogPost({ params }: { params: Promise&amp;lt;{ slug: string }&amp;gt; }) {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(r =&amp;gt; r.json());
  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;div dangerouslySetInnerHTML={{ __html: post.content }} /&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;generateStaticParams&lt;/code&gt; function tells Next.js which slugs to pre-render at build time as part of static site generation. Setting &lt;code&gt;revalidate = 600&lt;/code&gt; on the module activates ISR. After ten minutes, the next request triggers a background regeneration. The page still serves instantly from cache while the fresh version is being built. This combination is the standard pattern for content-heavy sites: fast initial load time from static files, with eventual consistency as content changes.&lt;/p&gt;
&lt;h3&gt;SEO implications&lt;/h3&gt;
&lt;p&gt;Search engines index HTML content. A CSR React application sends an empty &lt;code&gt;div&lt;/code&gt;; the content arrives only after JavaScript executes, which introduces indexing uncertainty and delays. Googlebot does execute JavaScript, but not instantly, and social media crawlers (Open Graph, Twitter Cards) typically do not execute JavaScript at all, meaning link previews for a CSR app will be blank.&lt;/p&gt;
&lt;p&gt;Next.js pages rendered via server-side rendering or static site generation (SSG) deliver fully populated HTML on the initial response. You can set metadata like page titles, descriptions, Open Graph tags, and canonical URLs using the built-in Metadata API routes, which generate the correct &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tags at render time on the server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/products/[id]/page.tsx

import type { Metadata } from &apos;next&apos;;

interface PageProps {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
}

export async function generateMetadata({ params }: PageProps): Promise&amp;lt;Metadata&amp;gt; {
  const { id } = await params;
  const product = await fetch(`/api/products/${id}`).then(r =&amp;gt; r.json());
  return {
    title: `${product.name} - Acme Store`,
    description: product.description,
    openGraph: {
      title: product.name,
      images: [{ url: product.imageUrl }],
    },
  };
}

export default async function ProductPage({ params }: PageProps) {
  const { id } = await params;
  const product = await fetch(`/api/products/${id}`).then(r =&amp;gt; r.json());
  return &amp;lt;ProductDetail product={product} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the fetch in &lt;code&gt;generateMetadata&lt;/code&gt; and the fetch in the page component both request the same URL. Next.js deduplicates these automatically using the built-in fetch cache. The network request is made once, even though you wrote it twice. This is a concrete example of the framework reducing accidental complexity.&lt;/p&gt;
&lt;p&gt;Quick reference for React vs Next.js:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/next-js-vs-react/next-js-vs-react-comparison.png&quot; alt=&quot;Quick reference for Next.js vs React&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;When to use React on its own&lt;/h2&gt;
&lt;p&gt;Standalone React (scaffolded with Vite or Create React App) is the appropriate choice when your application does not need server-side rendering, has no SEO requirements, and benefits from a thinner dependency footprint. The most common category is authenticated internal tooling: admin dashboards, CRM interfaces, data visualization tools, analytics platforms, and developer consoles. These applications sit behind a login screen, and the route structure is driven by application state (using state management libraries) rather than URL semantics.&lt;/p&gt;
&lt;p&gt;React can also be extended beyond the web using React Native, allowing you to reuse concepts and patterns when building mobile apps.&lt;/p&gt;
&lt;p&gt;A Vite-based React setup produces a minimal project with fast web development server startup and highly optimized production builds via Rollup. There is no server process to manage, no framework conventions to learn, and no build-time rendering pipeline to reason about.&lt;/p&gt;
&lt;p&gt;If your team is familiar with React but not with Next.js-specific concepts like the App Router, Server Components, or the distinction between server-side and client-side data fetching, a plain React setup removes that cognitive surface area.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Scaffold a React + TypeScript app with Vite
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

# Install Router for client-side navigation
npm install react-router-dom

# Install TanStack Query for data fetching and caching
npm install @tanstack/react-query
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This combination of Vite, React Router, and TanStack Query covers the needs of most internal web applications. TanStack Query handles caching, background refetching, loading, and error states, among other things&#x2014;with considerably less boilerplate than manually managing state with &lt;code&gt;useEffect&lt;/code&gt;. For web applications where all data fetching is client-side anyway, this stack is competitive with Next.js in terms of developer experience.&lt;/p&gt;
&lt;h3&gt;Integrating React into an existing back-end&lt;/h3&gt;
&lt;p&gt;Another valid use of standalone React is when you have an existing server-side framework like Rails, Django, Laravel, or Spring, and you want to embed React components into specific pages rather than migrate to a JavaScript-first stack.&lt;/p&gt;
&lt;p&gt;Third-party tools like Inertia.js let Rails or Laravel applications render React components server-side and manage navigation without a full SPA migration. In this architecture, Next.js would be redundant: the host framework already handles routing and server rendering.&lt;/p&gt;
&lt;h2&gt;When Next.js makes more sense&lt;/h2&gt;
&lt;p&gt;Next.js makes more sense when your application needs server-side rendering (SSR) for SEO or performance, when you want to colocate your API routes with your front-end code, or when you are building a content-heavy site where static site generation with revalidation provides both performance and freshness.&lt;/p&gt;
&lt;h3&gt;Public-facing applications with SEO requirements&lt;/h3&gt;
&lt;p&gt;Marketing sites, e-commerce websites, documentation, blogs, SaaS landing pages, and any application where pages need to rank in search results are natural fits for Next.js. Unlike React, the ability to combine static generation for high-traffic stable pages with ISR for frequently changing content, and server-side rendering (SSR) for personalized or session-dependent pages&#x2014;all within a single codebase&#x2014;is architecturally easy to implement.&lt;/p&gt;
&lt;p&gt;Consider a product listing page on an e-commerce site. You want the page to load instantly (static or ISR for performance), include accurate metadata for search engines and social sharing (server-side rendering or static site generation (SSG) for SEO), and show personalized pricing or stock information (partial hydration with a Client Component making a client-side fetch after the static shell renders).&lt;/p&gt;
&lt;p&gt;All of this is expressible in Next.js with standard patterns; in a plain React SPA, it requires either a separate server-side rendering (SSR) infrastructure or accepting the SEO limitations of client-side rendering.&lt;/p&gt;
&lt;h3&gt;Full-stack applications without a separate API service&lt;/h3&gt;
&lt;p&gt;Next.js Route Handlers let you define API endpoints that run alongside your front-end code, share type definitions, and deploy together as a single unit. For web development projects where the API and the UI are maintained by the same team and the back-end complexity does not justify a separate service, this is a significant simplification:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/api/orders/route.ts - runs on the server, not in the browser

import { NextRequest, NextResponse } from &apos;next/server&apos;;
import { db } from &apos;@/lib/db&apos;; // direct database access from the API route
import { auth } from &apos;@/lib/auth&apos;;

export async function GET(request: NextRequest) {
  const session = await auth();
  if (!session) {
    return NextResponse.json({ error: &apos;Unauthorized&apos; }, { status: 401 });
  }

  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get(&apos;page&apos;) ?? &apos;1&apos;, 10);
  const limit = 20;

  const orders = await db.order.findMany({
    where: { userId: session.user.id },
    orderBy: { createdAt: &apos;desc&apos; },
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({ orders, page });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This API route has direct database access, it imports a Prisma client, queries the database, and returns JSON. The front-end components in the same codebase can call this endpoint with a simple &lt;code&gt;fetch(&apos;/api/orders&apos;)&lt;/code&gt;. TypeScript types for the response can be shared between the route handler and the calling component without publishing a separate package or running a code generation step. For small to medium teams building full-stack web applications, this tight coupling is a feature, not a liability.&lt;/p&gt;
&lt;h3&gt;Server Actions for form handling and mutations&lt;/h3&gt;
&lt;p&gt;Next.js Server Actions allow you to define server-side mutation functions that can be called directly from Client Components without going through an explicit REST endpoint. This is the pattern to reach for when you need form submissions, data mutations, or any user-triggered server-side operation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/actions/createPost.ts
&apos;use server&apos;;

import { revalidatePath } from &apos;next/cache&apos;;
import { db } from &apos;@/lib/db&apos;;
import { auth } from &apos;@/lib/auth&apos;;

export async function createPost(formData: FormData) {
  const session = await auth();
  if (!session) throw new Error(&apos;Unauthenticated&apos;);

  const title = formData.get(&apos;title&apos;) as string;
  const content = formData.get(&apos;content&apos;) as string;

  await db.post.create({
    data: {
      title,
      content,
      authorId: session.user.id,
    },
  });

  revalidatePath(&apos;/blog&apos;); // purge the cached blog listing
}

// app/blog/new/page.tsx
&apos;use client&apos;;

import { createPost } from &apos;@/app/actions/createPost&apos;;

export default function NewPostForm() {
  return (
    &amp;lt;form action={createPost}&amp;gt;
      &amp;lt;input name=&amp;quot;title&amp;quot; placeholder=&amp;quot;Post title&amp;quot; /&amp;gt;
      &amp;lt;textarea name=&amp;quot;content&amp;quot; placeholder=&amp;quot;Write your post...&amp;quot; /&amp;gt;
      &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Publish&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The form&apos;s &lt;code&gt;action&lt;/code&gt; prop accepts the &lt;code&gt;createPost&lt;/code&gt; server action directly. When submitted, Next.js serializes the form data and sends it to the server function, which executes in the Node.js runtime with full access to the database and environment variables. The &lt;code&gt;revalidatePath&lt;/code&gt; call purges the Next.js cache for the &lt;code&gt;/blog&lt;/code&gt; route, so the updated post list appears on the next request without a manual cache invalidation step. This entire pattern (form, mutation, cache invalidation) requires no custom API endpoint.&lt;/p&gt;
&lt;h2&gt;Next.js vs React: The architectural decision&lt;/h2&gt;
&lt;p&gt;The separation is cleaner than it might initially appear. React alone handles use cases where the application runs entirely in the browser and focuses on user interfaces through a component-based architecture. It&apos;s ideal for internal tools, authenticated dashboards, and single-page web applications without SEO requirements. This includes building interactive user interfaces for internal tooling, where client-side rendering is perfectly acceptable. Add Next.js when your web application needs to deliver server-rendered HTML for SEO or performance, when you want to run server-side logic alongside your front-end without maintaining a separate service, or when the file-system router and built-in optimizations justify the additional framework layer.&lt;/p&gt;
&lt;p&gt;The practical signal to watch for is the nature of your first meaningful page render. If a blank loading screen is acceptable because the web application sits behind &lt;a href=&quot;https://www.honeybadger.io/blog/javascript-authentication-guide/&quot;&gt;authentication&lt;/a&gt; and your users do not discover it via search, React&apos;s CSR model is okay. If search engine visibility is important, or the site needs to be fast for unauthenticated users on variable network connections, the server-side rendering capabilities of Next.js are important features.&lt;/p&gt;
&lt;p&gt;One area worth monitoring is the Server Components model. The RSC architecture changes how you reason about data fetching and bundle size in ways that are not entirely settled. The mental model of interleaving server and client components, managing cache invalidation with &lt;code&gt;revalidatePath&lt;/code&gt; and &lt;code&gt;revalidateTag&lt;/code&gt;, and understanding which dependencies run only on the server has a meaningful learning curve. For developers new to both React and Next.js, starting with plain React as a JavaScript library and introducing Next.js only when you need server-side rendering or static site generation is a lower-risk path than beginning with the full App Router feature set.&lt;/p&gt;
&lt;p&gt;If you liked this article and want to learn more about JavaScript, join the &lt;a href=&quot;https://www.honeybadger.io/newsletter/javascript/&quot;&gt;Honeybadger newsletter&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>SIEM alerts: everything you need to know</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/siem-alerts/"/>
    <id>https://www.honeybadger.io/blog/siem-alerts/</id>
    <published>2026-05-21T07:00:00+00:00</published>
    <updated>2026-05-21T07:00:00+00:00</updated>
    <author>
      <name>Muhammed Ali</name>
    </author>
    <summary type="text">SIEM alerts help you detect suspicious behavior before it becomes a breach. But security monitoring can quickly turn into noisy dashboards and missed threats without the right approach. Read this article to learn how to design effective SIEM alerts and implement real-time security monitoring.</summary>
    <content type="html">&lt;p&gt;Let&apos;s walk through setting up SIEM (Security Information and Event Management) alerts to monitor security threats in applications. We will explain what SIEM alerts are, why they&apos;re relevant with regard to application security, and provide practical examples of common alerts a developer could implement. We will show how to configure simple alerts with Honeybadger Insights.&lt;/p&gt;
&lt;h2&gt;What is SIEM?&lt;/h2&gt;
&lt;p&gt;SIEM (Security Information and Event Management) refers to a class of security platforms that aggregate logs and security events from many systems and analyze them to detect threats. Just like in action movies where a thief exploits an unguarded angle, attackers in the cyber world look for weaknesses. A SIEM system works by pulling data from many different sources across the organization, including security system logs, usual and unusual network traffic, and threat intelligence. The SIEM analyzes them in one place to detect suspicious behavior instead of these signals living in silos.&lt;/p&gt;
&lt;p&gt;The main aim of SIEM is to be a correlation engine. It doesn&#x2019;t just collect raw data; it connects the dots using rule-based correlation, statistical analysis, and sometimes machine learning. The SIEM system continuously evaluates events in real time to identify patterns that indicate a real attack, enabling faster threat detection across your environment. Correlation helps prioritize alerts, not necessarily reduce them automatically. What this means is fewer false positives and clearer signals that something genuinely malicious is happening.&lt;/p&gt;
&lt;h2&gt;The importance of SIEM alerts&lt;/h2&gt;
&lt;p&gt;Organizations often run dozens or even hundreds of disconnected security tools where each generates alerts that require attention. Analysts are forced to jump between dashboards and manually investigate events. SIEM system addresses this problem by acting as the central nervous system of security operations. It prioritizes alerts by severity to help analysts immediately focus on incidents that pose the greatest risk.&lt;/p&gt;
&lt;p&gt;The attackers don&apos;t even discriminate based on the size of the organisation. Some do it for the challenge or fun of it. They target organizations of all sizes and take advantage of openings across applications and cloud infrastructure. A SIEM system provides the visibility and context needed to detect these potential threats early, before they cause serious damage. Much like ignoring a staged diversion at the front of a museum and spotting the real break-in at the back, SIEM becomes one of the most powerful defensive tools security teams have.&lt;/p&gt;
&lt;h2&gt;How to respond to SIEM alerts effectively&lt;/h2&gt;
&lt;p&gt;Responding to SIEM alerts effectively determines whether a security incident is quickly contained or allowed to turn into a breach. Alert incident response procedures must be built on preparation, documentation, and practiced workflows. The goal is not just to react, but to respond effectively.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Establish runbooks for common alert types&lt;/strong&gt;: Runbooks define exactly what should happen when a specific alert is triggered. For example, when a credential stuffing alert fires, the runbook should outline verification steps such as checking whether multiple accounts are affected, containment actions like blocking the attacking IP range, and notification requirements, including informing affected users. By standardizing incident responses, runbooks ensure consistency and help less-experienced team members handle incidents correctly without improvising under pressure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Triage alerts immediately upon receipt&lt;/strong&gt;: The first moments after receiving an alert are very important. Spend some time determining whether the alert represents a genuine threat or requires deeper investigation. Review recent similar alerts to see if the event is part of a broader attack pattern. You can also &lt;a href=&quot;https://docs.honeybadger.io/guides/insights/badgerql/&quot;&gt;query the SIEM&lt;/a&gt; using BadgerQL (Honeybadger&apos;s Query Language) for additional context.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Document investigation steps and findings&lt;/strong&gt;: Every alert investigation should leave a record. When alerts turn out to be false, document why they were triggered and whether tuning adjustments can prevent recurrence. When alerts uncover real attacks, record the attack vector, affected systems, and remediation steps taken.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Implement automated containment where safe&lt;/strong&gt;: Certain scenarios, such as IP-based attacks, benefit from automated containment. When an IP triggers credential stuffing alerts, it can be temporarily blocked at the firewall or web application firewall. This allows containment to happen within seconds rather than waiting for manual action. However, automation must be used carefully. Over-automation can disrupt legitimate users.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Coordinate incident response across teams&lt;/strong&gt;: Security incidents often span multiple layers, including applications, infrastructure, and databases. The security team may analyze data of the attack method, development teams patch vulnerable code, and security operations teams apply network-level blocks. Clear communication channels are essential. Many organizations rely on dedicated Slack channels or conference bridges to coordinate effectively during active incidents.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SIEM alert examples and types&lt;/h2&gt;
&lt;p&gt;SIEM alerts fall into several categories based on detection method and threat type. Each category serves a distinct purpose in the comprehensive security monitoring efforts to improve the security posture. Some identify known attack patterns, and some detect subtle behavioral deviations and enforce regulations. Below is a list of SIEM alerts covering the top SIEM alerts by threat detection method and also internal and external threat types.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Signature alerts&lt;/strong&gt;: Signature-based alerts match specific patterns in log data, such as SQL injection attempts in HTTP requests or known malware file hashes. These alerts trigger when logs contain exact strings or regular expression matches associated with attacks. For example, detecting &lt;code&gt;&apos; OR &apos;1&apos;=&apos;1&apos;&lt;/code&gt; in query parameters signals a potential SQL injection probe.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Anomaly alerts&lt;/strong&gt;: Anomaly-based alerts establish behavioral baselines and flag deviations. If a user account typically authenticates from New York during business hours, a login from Singapore at 3 AM exceeds normal behavior thresholds. These alerts require sufficient historical data to build accurate profiles.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Threshold alerts&lt;/strong&gt;: Threshold-based alerts trigger when event counts exceed defined limits within time windows. Failed authentication attempts provide a clear example: five failed logins from a single IP address within ten minutes might indicate credential stuffing, while 100 failed logins across different accounts suggest a broader attack.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compliance threat alerts&lt;/strong&gt;: Compliance reporting enforces regulatory requirements and internal policies. PCI-DSS mandates alerts for unauthorized access attempts to cardholder data, while HIPAA requires notification of protected health information access outside normal workflows. Compliance frameworks require alerting and auditing, but tuning is still necessary to avoid alert fatigue.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Configuring alert triggers and correlation rules&lt;/h2&gt;
&lt;p&gt;Alert triggers define the conditions that generate notifications for common SIEM solution alerts. Simple triggers evaluate single log entries, while complex triggers correlate events across time windows and security data sources. Start with high-confidence signatures for known attacks before layering in anomaly detection and behavioral analysis.&lt;/p&gt;
&lt;p&gt;Authentication alerts should trigger on multiple unsuccessful login attempts, successful logins following failed attempts (credential stuffing success), logins from blacklisted IP addresses, and authentications occurring simultaneously from geographically distant locations. Configure these with appropriate thresholds; three failed attempts might be a typo, but fifteen suggests an attack. Time windows matter too; five failures over 24 hours are less significant than five failures in sixty seconds.&lt;/p&gt;
&lt;p&gt;Alerts on the application side monitor for injection attacks, path traversal attempts, and malicious file uploads. Web application firewalls generate logs that SIEM systems ingest and analyze. When requests attempt to access restricted file paths, or when uploaded files contain executable code. These alerts benefit from whitelisting safe patterns to reduce false positives from legitimate applications or user behavior.&lt;/p&gt;
&lt;p&gt;Data access alerts flag unusual database queries, excessive record retrieval, and access to sensitive data outside normal application workflows. A user downloading sensitive data, such as customer records, at 2 AM warrants investigation, even if their credentials authenticate successfully. Configure these alerts to understand normal data access patterns.&lt;/p&gt;
&lt;h2&gt;Reducing false positives through SIEM alerts best practices&lt;/h2&gt;
&lt;p&gt;Reducing false positives is a core part of SIEM system alerts best practices, as excessive noise diminishes trust in alerting systems. Tuning requires iterative refinement based on investigation outcomes and environmental knowledge.&lt;/p&gt;
&lt;p&gt;Whitelist known-safe activities that trigger alerts. Automated security scanners, monitoring systems, and internal tools often generate traffic patterns resembling attacks. Document these sources and exclude them from triggering alerts. For example, vulnerability scanners probe for SQL injection vulnerabilities as part of routine testing; their IP addresses should be whitelisted to prevent false alarms during scheduled scans.&lt;/p&gt;
&lt;p&gt;Context development could add business intelligence to raw security events. An alert showing &amp;quot;100 failed login attempts from 203.0.113.45&amp;quot; provides limited context. Combining this with evolving threat intelligence reveals whether the IP belongs to a known botnet, including geolocation, which shows the attack origin, and correlating with past incidents indicates if this IP has targeted the organization previously.&lt;/p&gt;
&lt;p&gt;Alert aggregation prevents duplicate notifications for the same incident. When an attacker probes multiple endpoints, each probe might trigger individual alerts. Aggregate these into a single incident showing the attack&apos;s scope rather than flooding the team with hundreds of similar notifications.&lt;/p&gt;
&lt;h2&gt;Managing alert fatigue and team burnout&lt;/h2&gt;
&lt;p&gt;Alert fatigue occurs when you receive so many notifications that they become desensitized, and you miss important security incidents. If your company receives hundreds of alerts a day, you&apos;re likely to get low investigation rates, with analysts ignoring the bulk of alerts.&lt;/p&gt;
&lt;p&gt;Implement alert scoring that combines severity, context, and historical accuracy. Alerts that frequently lead to confirmed security incidents receive higher scores than those with poor signal-to-noise ratios. Machine learning models can predict which alerts warrant investigation based on key components like time of day, user reputation scores, and historical attack patterns. This scoring helps security analysts prioritize workloads when alert volumes exceed capacity.&lt;/p&gt;
&lt;p&gt;Establish alert ownership and escalation paths. Each alert type needs a designated team responsible for investigation and remediation. Application security alerts route to security teams familiar with the codebase, infrastructure alerts go to operations, and access control alerts might escalate to the security team. Clear ownership prevents alerts from languishing in shared queues where everyone assumes someone else will investigate.&lt;/p&gt;
&lt;h2&gt;Setting up alerts with Honeybadger Insights&lt;/h2&gt;
&lt;p&gt;Applications generate a constant stream of events like failed logins, suspicious input, permission changes, and unexpected system or user behavior. Instead of sending raw log files into a traditional SIEM system pipeline and configuring complex agents, Honeybadger Insights is &lt;a href=&quot;https://www.honeybadger.io/tour/logging-observability/&quot;&gt;an observability tool&lt;/a&gt; that gives you structured security events directly from your application. These events become searchable, filterable, and alertable. This allows you to detect and respond to potential threats in real time without heavy infrastructure.&lt;/p&gt;
&lt;p&gt;The idea is simple: treat security threats like application telemetry. Whenever something suspicious happens, you send a structured event to Honeybadger Insights. Then you create queries and alerts that behave like SIEM system rules.&lt;/p&gt;
&lt;p&gt;For this guide, we will work with Nodejs. First, install the Honeybadger JavaScript package in your Node.js application. This example uses a basic Express server that reports failed login attempts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm init -y
npm install express @honeybadger-io/js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a file named &lt;code&gt;server.js&lt;/code&gt; and configure Honeybadger.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const express = require(&amp;quot;express&amp;quot;);
const Honeybadger = require(&amp;quot;@honeybadger-io/js&amp;quot;);
const app = express();

app.use(express.json());

// Initialize Honeybadger
Honeybadger.configure({
  apiKey: process.env.HONEYBADGER_API_KEY,
  environment: &amp;quot;production&amp;quot;,
});

// Simulated login endpoint
app.post(&amp;quot;/login&amp;quot;, (req, res) =&amp;gt; {
  const { username, password } = req.body;

  // Fake authentication logic
  const isValid = username === &amp;quot;admin&amp;quot; &amp;amp;&amp;amp; password === &amp;quot;secret&amp;quot;;

  if (!isValid) {
    // Send a SIEM-style security event to Honeybadger Insights
    Honeybadger.event({
      event_type: &amp;quot;security.login.failed&amp;quot;,
      user: username,
      ip: req.ip,
      timestamp: new Date().toISOString(),
      metadata: {
        reason: &amp;quot;Invalid credentials&amp;quot;,
      },
    });

    return res.status(401).json({ error: &amp;quot;Invalid credentials&amp;quot; });
  }

  res.json({ message: &amp;quot;Login successful&amp;quot; });
});

// Example: suspicious input detection
app.post(&amp;quot;/search&amp;quot;, (req, res) =&amp;gt; {
  const { query } = req.body;

  if (query &amp;amp;&amp;amp; query.includes(&amp;quot;&apos; OR 1=1&amp;quot;)) {
    Honeybadger.event({
      event_type: &amp;quot;security.sql_injection.detected&amp;quot;,
      ip: req.ip,
      query,
      timestamp: new Date().toISOString(),
    });
  }

  res.json({ results: [] });
});

app.listen(3000, () =&amp;gt; {
  console.log(&amp;quot;Server running on port 3000&amp;quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this setup, each security action is shown as a structured event. Instead of parsing logs later, Honeybadger stores these events in Insights, where they can be queried like a lightweight SIEM system dataset.&lt;/p&gt;
&lt;p&gt;To run the code locally, create a &lt;code&gt;.env&lt;/code&gt; file or export your API key in the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export HONEYBADGER_API_KEY=your_api_key_here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node server.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then simulate events using curl or Postman:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST http://localhost:3000/login \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &apos;{&amp;quot;username&amp;quot;:&amp;quot;admin&amp;quot;,&amp;quot;password&amp;quot;:&amp;quot;wrong&amp;quot;}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each failed request will appear in Honeybadger Insights within seconds. You can repeat the request multiple times to trigger your alert thresholds.&lt;/p&gt;
&lt;p&gt;After events start flowing, you can configure SIEM-style alerts inside Honeybadger. For example, here&apos;s a query to find the &lt;code&gt;security.login.failed&lt;/code&gt; events:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;fields @ts, @preview
| filter event_type::str == &amp;quot;security.login.failed&amp;quot;
| sort @ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honeybadger can alert you when the event count exceeds a threshold within a time window. This allows you to detect brute-force attacks, suspicious traffic spikes, or repeated injection attempts. You can also filter by IP address, user, environment, or any custom alert metadata you send.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/siem-alerts/honeybadger-insights-query.png&quot; alt=&quot;Honeybadger Insights dashboard for SIEM alerts&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After configuring the Insights query, you can create an alarm by clicking the triple dots in the events panel. Once you&apos;ve selected &amp;quot;Build alarm&amp;quot;, you&apos;ll be taken to a new setup screen that looks something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/siem-alerts/alerts-in-depth.png&quot; alt=&quot;An in-depth look at an active alert showing the Insights query.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here, the alert has already been triggered&#x2014;you can set thresholds, lag times, and other settings. Alarms also have their own dashboard, where you can find an overview of their status.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/siem-alerts/alert-menu.png&quot; alt=&quot;Alert homepage&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This approach turns your application into the primary source of security intelligence. Instead of forwarding system logs and building fragile parsing rules, Honeybadger alerts your team about events at the moment the risk occurs. The result is faster threat detection, cleaner security data, and SIEM-style security monitoring without the operational overhead of traditional security pipelines.&lt;/p&gt;
&lt;p&gt;If you want to take this further, you can extend the pattern to permission changes, rate-limit violations, token misuse, or unusual API access patterns. You can read more about everything queries can do in the &lt;a href=&quot;https://docs.honeybadger.io/guides/insights/&quot;&gt;Insights documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Benefits of Honeybadger for SIEM alerts&lt;/h2&gt;
&lt;p&gt;Traditional enterprise SIEM solutions require significant investment in licensing, infrastructure, and specialized personnel. Other security solutions like Splunk, QRadar, and ArcSight need you to have a dedicated security operations center and security teams trained in complex query languages. These platforms also require deep integration work to connect with your existing infrastructure and other security tools already in your stack.&lt;/p&gt;
&lt;p&gt;That&apos;s not the case for Honeybadger compared to other tools used for security event management. It removes these barriers through developer-focused integration, so you focus more on improving security posture. Setup takes minutes, and installing the package requires just a few commands. The platform automatically captures events through existing error and performance monitoring instrumentation, which lets you enhance threat detection capabilities (improve security posture) without overhauling your entire toolchain.&lt;/p&gt;
&lt;p&gt;Alert configuration happens through intuitive web interfaces. Creating a SIEM solution alert resembles configuring a performance threshold, which involves selecting the event pattern, defining the threshold, and specifying notification channels. Structured logging lets you query and analyze data immediately. Notifications integrate seamlessly through Slack, PagerDuty, and webhooks. The platform combines SIEM system capabilities with error tracking and performance monitoring in a single interface to provide a unified dashboard for security incident investigation.&lt;/p&gt;
&lt;p&gt;You can sign up for a &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;free trial of Honeybadger&lt;/a&gt; to see if this will fit your company&#x2019;s needs.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Test-Commit-Revert: A useful workflow for testing legacy code in Ruby</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/ruby-tcr-test-commit-revert/"/>
    <id>https://www.honeybadger.io/blog/ruby-tcr-test-commit-revert/</id>
    <published>2020-10-06T00:00:00+00:00</published>
    <updated>2026-05-14T00:00:00+00:00</updated>
    <author>
      <name>Jos&#xe9; M. Gilgado</name>
    </author>
    <summary type="text">When you inherit a legacy app with no tests, your first step should be to add them. But that can be a huge task! How do you even start? In this article, Jos&#xe9; will introduce us to a testing workflow called test-commit-revert (TCR) that is particularly useful for adding tests to legacy systems. Read to see practical examples and how to set up your tooling for minimal friction.</summary>
    <content type="html">&lt;p&gt;It happens to all of us. As software projects grow, parts of the production code we ship end up without a comprehensive test suite. When you take another look at the same area of code after a few months, it may be difficult to understand; even worse, there might be a bug, and we don&apos;t know where to begin fixing it.&lt;/p&gt;
&lt;p&gt;Modifying production code without tests is a major challenge. We can&apos;t be sure if we&apos;ll break anything in the process, and checking everything manually is, at best, prone to mistakes; usually, it&apos;s impossible.&lt;/p&gt;
&lt;p&gt;Dealing with this kind of code is one of the most common tasks we perform as developers, and many techniques have focused on this issue over the years, such as &lt;a href=&quot;https://www.honeybadger.io/blog/ruby-legacy-characterization-test/&quot;&gt;characterization tests&lt;/a&gt;, which we discussed in a previous article.&lt;/p&gt;
&lt;p&gt;Today, we&apos;ll cover another technique based on characterization tests (test-commit-revert) and introduced by Kent Beck, who also introduced TDD to the modern programming world many years ago.&lt;/p&gt;
&lt;h2&gt;What&apos;s TCR?&lt;/h2&gt;
&lt;p&gt;TCR stands for &amp;quot;test, commit, revert&amp;quot;, but it&apos;s more accurate to call it &amp;quot;test &amp;amp;&amp;amp; commit || revert&amp;quot;. Let&apos;s see why.&lt;/p&gt;
&lt;p&gt;This technique describes a workflow to test legacy code. We&apos;ll use a script that will run the tests every time we save our project files. The process is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, we create an empty unit test for the part of the legacy code we want to test.&lt;/li&gt;
&lt;li&gt;We then add a single assertation and save the test.&lt;/li&gt;
&lt;li&gt;Since we have our script set up, the test is automatically run. If it succeeds, the change is committed. If the test fails, the change is deleted (reverted), and we need to try again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the test passes, we can then add a new test case.&lt;/p&gt;
&lt;p&gt;Essentially, TCR (test, commit, revert) is about keeping your code in a &amp;quot;green&amp;quot; state instead of writing a failing test first (red) and then make it pass (green), as we do with test-driven development. If we write a failing test, it&apos;ll just vanish, and we&apos;ll be brought back to the &amp;quot;green&amp;quot; state again.&lt;/p&gt;
&lt;h2&gt;Purpose of test-commit-revert&lt;/h2&gt;
&lt;p&gt;The main goal of this technique is to understand the code a bit better each time you add a test case. This will naturally increase the test coverage and unblock many refactorings that, otherwise, wouldn&apos;t be possible.&lt;/p&gt;
&lt;p&gt;One of the advantages of test, commit, revert is that it&apos;s useful in many scenarios. We can use it with production code that has no tests at all or with code that&apos;s partially tested. If the tests fail, we just revert the change and try again.&lt;/p&gt;
&lt;h2&gt;How can we use it?&lt;/h2&gt;
&lt;p&gt;Kent Beck shows, in different articles and videos (linked at the end), that a good approach is using a script that runs after certain files in the project are saved.&lt;/p&gt;
&lt;p&gt;This will depend heavily on the project you&apos;re trying to test. Something like the following script, which is executed every time we save files with a plugin in the editor, is a good start:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;(rspec &amp;amp;&amp;amp; git commit -am &amp;quot;WIP&amp;quot;) || git reset --hard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re using Visual Studio Code, a good plugin to execute on every save is &lt;a href=&quot;https://github.com/emeraldwalk/vscode-runonsave&quot;&gt;&amp;quot;runonsave&amp;quot;&lt;/a&gt;. You can include the above command or a similar one for your project. In this case, the whole config file would be&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;folders&amp;quot;: [{ &amp;quot;path&amp;quot;: &amp;quot;.&amp;quot; }],
  &amp;quot;settings&amp;quot;: {
    &amp;quot;emeraldwalk.runonsave&amp;quot;: {
      &amp;quot;commands&amp;quot;: [
        {
          &amp;quot;match&amp;quot;: &amp;quot;*.rb&amp;quot;,
          &amp;quot;cmd&amp;quot;: &amp;quot;cd ${workspaceRoot} &amp;amp;&amp;amp; rspec &amp;amp;&amp;amp; git commit -am WIP || git reset --hard&amp;quot;
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember that later, you can squash the commit with Git directly in the command line or when merging the PR if you&apos;re using Github:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/ruby-tcr-test-commit-revert/squash-github.png&quot; alt=&quot;Squash commits on Github&quot; title=&quot;Squash commits on Github&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This means we&apos;ll only get one commit in the main branch for all the commits we did on the branch we&apos;re working on. This diagram from Github explains it well:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.honeybadger.io/images/blog/posts/ruby-tcr-test-commit-revert/commit-squashing-diagram.png&quot; alt=&quot;Diagram squashed commits on Github&quot; title=&quot;test commit revert: Diagram squashed commits on Github&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Writing our first test with TCR&lt;/h2&gt;
&lt;p&gt;We&apos;ll use a simple example to illustrate the technique. We have a class that we know is working, but we need to modify it.&lt;/p&gt;
&lt;p&gt;We could just make a change and deploy the changes to production. However, we want to be sure that we don&apos;t break anything in the process, which is always a good idea.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# worker.rb
class Worker
  def initialize(age, active_years, veteran)
    @age = age
    @active_years = active_years
    @veteran = veteran
  end

  def can_retire?
    return true if @age &amp;gt;= 67
    return true if @active_years &amp;gt;= 30
    return true if @age &amp;gt;= 60 &amp;amp;&amp;amp; @active_years &amp;gt;= 25
    return true if @veteran &amp;amp;&amp;amp; @active_years &amp;gt; 25

    false
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first step would be to create a new file for the tests, so we can start adding them there. We&apos;ve seen the first line in the &lt;code&gt;can_retire?&lt;/code&gt; method with&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;  def can_retire?
    return true if @age &amp;gt;= 67
    ...
    ...
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thus, we can test this case first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# specs/worker_spec.rb
require_relative &apos;./../worker&apos;

describe Worker do
  describe &apos;can_retire?&apos; do
    it &amp;quot;should return true if age is higher than 67&amp;quot; do

    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s a quick tip: when you&apos;re working with test, commit, revert, every time you save, the latest changes will disappear if the tests fail. Therefore, we want to have as much code as possible to &amp;quot;set up&amp;quot; the test before actually writing and saving the line or lines with the assertion.&lt;/p&gt;
&lt;p&gt;If we save the above file like that, we can then add a line for the test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;require_relative &apos;./../worker&apos;

describe Worker do
  describe &apos;can_retire?&apos; do
    it &amp;quot;should return true if age is higher than 67&amp;quot; do
      expect(Worker.new(70, 10, false).can_retire?).to be_true ## This line can disappear when we save now
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we save, if the new line doesn&apos;t vanish, we&apos;ve done a good job; the test passes!&lt;/p&gt;
&lt;h2&gt;Adding more tests&lt;/h2&gt;
&lt;p&gt;Once we have our first test, we can keep adding more cases while taking into account false cases. After some work, we have something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# frozen_string_literal: true

require_relative &apos;./../worker&apos;

describe Worker do
  describe &apos;can_retire?&apos; do
    it &apos;should return true if age is higher than 67&apos; do
      expect(Worker.new(70, 10, false).can_retire?).to be true
    end

    it &apos;should return true if age is 67&apos; do
      expect(Worker.new(67, 10, false).can_retire?).to be true
    end

    it &apos;should return true if age is less than 67&apos; do
      expect(Worker.new(50, 10, false).can_retire?).to be false
    end

    it &apos;should return true if active years is higher than 30&apos; do
      expect(Worker.new(60, 31, false).can_retire?).to be true
    end

    it &apos;should return true if active years is 30&apos; do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In every case, we write the &amp;quot;it&amp;quot; block first, save, and then add the assertion with &lt;code&gt;expect(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As usual, we can add as many tests as possible, but it makes sense to avoid adding too many once we&apos;re relatively sure that everything is covered.&lt;/p&gt;
&lt;p&gt;There are still a few cases to cover, so we should add them just for completeness.&lt;/p&gt;
&lt;h2&gt;Final tests&lt;/h2&gt;
&lt;p&gt;Here&apos;s the spec file in its final form. As you can see, we could still add more cases, but I think this is enough to illustrate the process of TCR.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# frozen_string_literal: true

require_relative &apos;./../worker&apos;

describe Worker do
  describe &apos;can_retire?&apos; do
    it &apos;should return true if age is higher than 67&apos; do
      expect(Worker.new(70, 10, false).can_retire?).to be true
    end

    it &apos;should return true if age is 67&apos; do
      expect(Worker.new(67, 10, false).can_retire?).to be true
    end

    it &apos;should return true if age is less than 67&apos; do
      expect(Worker.new(50, 10, false).can_retire?).to be false
    end

    it &apos;should return true if active years is higher than 30&apos; do
      expect(Worker.new(60, 31, false).can_retire?).to be true
    end

    it &apos;should return true if active years is 30&apos; do
      expect(Worker.new(20, 30, false).can_retire?).to be true
    end

    it &apos;should return true if age is higher than 60 and active years is higher than 25&apos; do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end

    it &apos;should return true if age is higher than 60 and active years is higher than 25&apos; do
      expect(Worker.new(61, 30, false).can_retire?).to be true
    end

    it &apos;should return true if age is 60 and active years is higher than 25&apos; do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end

    it &apos;should return true if age is higher than 60 and active years is 25&apos; do
      expect(Worker.new(61, 25, false).can_retire?).to be true
    end

    it &apos;should return true if age is 60 and active years is 25&apos; do
      expect(Worker.new(60, 25, false).can_retire?).to be true
    end

    it &apos;should return true if is veteran and active years is higher than 25&apos; do
      expect(Worker.new(60, 25, false).can_retire?).to be true
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ways to refactor&lt;/h2&gt;
&lt;p&gt;If you&apos;ve read this far, there&apos;s probably something that feels a bit off with the code. We have many &amp;quot;magical numbers&amp;quot; that should be extracted into constants, both in the test and in the Worker class.&lt;/p&gt;
&lt;p&gt;We could also create private methods for each case in the main can_retire? public method.&lt;/p&gt;
&lt;p&gt;I&apos;ll leave both potential refactorings as exercises for you. However, we have tests now, so if we make a mistake in any step, they will tell us.&lt;/p&gt;
&lt;h2&gt;Where do you go from here?&lt;/h2&gt;
&lt;p&gt;I encourage you to try test-commit-revert with your projects and production code. It&apos;s a very cheap experiment because you don&apos;t need any fancy continuous integration in an external server or a dependency with a new library. All you need is a way to execute a command every time you save certain files on your computer.&lt;/p&gt;
&lt;p&gt;It&apos;ll also give you a &amp;quot;gaming&amp;quot; experience when adding tests, which is always fun and interesting. Additionally, the discipline of having failing tests removed from your editor (a safety measure that kicks in whenever tests fail) will give you an extra safety net by confirming that the tests you&apos;re pushing to the repository are actually passing.&lt;/p&gt;
&lt;p&gt;I hope you find this new technique useful when dealing with legacy code. I&apos;ve used multiple times in the last few months, and it&apos;s always been a pleasure.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Tips for upgrading Python/Django versions in existing apps</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/tips-for-upgrading-python-django-versions-in-existing-apps/"/>
    <id>https://www.honeybadger.io/blog/tips-for-upgrading-python-django-versions-in-existing-apps/</id>
    <published>2023-07-20T00:00:00+00:00</published>
    <updated>2026-05-05T00:00:00+00:00</updated>
    <author>
      <name>Michael Barasa</name>
    </author>
    <summary type="text">Unlock the power of the latest Python and Django versions with expert tips for seamlessly upgrading Python apps.</summary>
    <content type="html">&lt;p&gt;Python is a robust and powerful programming language. In addition to machine learning, Python can be used for tasks such as web scraping, image processing, scientific computing, and much more. A framework such as Django, which is built on top of Python, enables you to build beautiful web applications&#x2014;top websites such as Dropbox, Instagram, and YouTube use Django.&lt;/p&gt;
&lt;p&gt;However, as you create applications and release them to consumers, upgrading to the latest Python version becomes essential to keep pace with updates and new features. For instance, Python 3.11 was released in October 2022, sporting various bug fixes. Before that, there was &lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;Python 3.10, 3.9, 3.8&lt;/a&gt;, and others. There are also numerous third-party libraries that you should regularly watch out for.&lt;/p&gt;
&lt;p&gt;Incorporating these updates into our applications can be beneficial, as doing so can enhance the security and reliability. However, doing this incorrectly could also break your application.&lt;/p&gt;
&lt;h2&gt;Why you should be upgrading your Python version&lt;/h2&gt;
&lt;p&gt;Upgrading your Python and Django applications to the latest Python version is important for several reasons.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;New Python versions come with additional features and improvements. Newer Django versions can help reduce boilerplate code, enabling you to develop and push products to the market much faster. They can also help you learn new ways of adding functionalities to your application that can be more fun and easier to implement.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;New Python versions can help with bug fixes in your code. Apart from enabling your application to behave as expected, eliminating bugs also streamlines the software development process, meaning that you&apos;ll be less frustrated. A Python upgrade can improve your app&apos;s stability during production.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Upgrading your application also makes it easier to maintain your codebase. You can quickly incorporate changes in your project without major downtimes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ensuring you&apos;re using the latest Python version and Django version also improves security. Hackers are always looking for new ways to infiltrate systems, steal data, install malicious files and viruses, and just wreak havoc. Upgrading your application to use the latest Python version or Django ensures that you have the latest security updates.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now that we understand why upgrading applications is necessary, let&apos;s jump into the action itself.&lt;/p&gt;
&lt;h2&gt;Things to know before updating&lt;/h2&gt;
&lt;p&gt;Before updating the Python version, you should read the changes in the newest Python release. Go through the official documentation, particularly the release notes. Note that with each new version, Django releases accompanying documentation, which is quite helpful when upgrading applications.&lt;/p&gt;
&lt;p&gt;Next, look at the functions, objects, and other items deprecated in the latest Python version. Consider the deadline or timeline in which these deprecation changes are set to come into effect. Understanding the deprecation timeline allows you to better plan your upgrade process, including the prioritization of certain tasks over others.&lt;/p&gt;
&lt;p&gt;You should also keep an eye on backwards compatibility. Remember that some things that work in your existing application may fail when you upgrade to a new Python version. This is why it&apos;s recommended to test things first before pushing to production. Whenever an upgrade causes your app to break, you can roll back to the previous Python version as you figure out the next steps.&lt;/p&gt;
&lt;h2&gt;How to update Python/Django apps&lt;/h2&gt;
&lt;p&gt;In this section, we will learn how to upgrade Python and Django in a simple Django Movie API to use a newer Python version. You can download the project&apos;s code from this &lt;a href=&quot;https://github.com/WanjaMIKE/simpledjangobookapi/&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Check Django and other library versions&lt;/h3&gt;
&lt;p&gt;Once you&apos;ve downloaded the Movie API project and finished setting it up on your computer, the first step is to check the versions of the dependencies that the project uses.&lt;/p&gt;
&lt;p&gt;To do this, launch the project in your preferred code editor and open the &lt;code&gt;requirements.txt&lt;/code&gt; file. All dependencies used by the project are listed in this file, as follows.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;asgiref==3.3.1
Django==3.1.8
django-filter==2.4.0
djangorestframework==3.12.2
djangorestframework-simplejwt==4.6.0
pytz==2021.1
sqlparse==0.4.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we now know the versions in use (shown above), the next step is to determine the current official version from the documentation. For instance, the latest official Django version is 4.2.1; our application uses an older version (3.1.8), so there&apos;s a need to upgrade.&lt;/p&gt;
&lt;h3&gt;Activate local debugging&lt;/h3&gt;
&lt;p&gt;Trying to update things in production could cause them to break and lead to poor user experience and other negative consequences. You should test your applications locally and ensure that everything is working before pushing them to production.&lt;/p&gt;
&lt;p&gt;You can activate local debugging by setting the &lt;code&gt;debug&lt;/code&gt; option in the &lt;code&gt;settings.py&lt;/code&gt; file to &lt;code&gt;True&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# settings.py
import os

# Build paths inside the project like this: os.path.join(BASE_DIR, &#x2026;)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/wa

# SECURITY WARNING: Don&#x2019;t run with debug turned on in production!
DEBUG = True  # Set debug to true

ALLOWED_HOSTS = []
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Run Django checks with python -Wall manage.py check&lt;/h3&gt;
&lt;p&gt;Before starting the upgrade, we&apos;ll need to run several checks to determine if there are any deprecation warnings in our project.&lt;/p&gt;
&lt;p&gt;Deprecation warnings show that certain features will stop working at some point simply because they have been replaced by better alternatives.&lt;/p&gt;
&lt;p&gt;You can check for any warnings using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;python -Wa manage.py test
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Install new dependencies&lt;/h3&gt;
&lt;p&gt;Now that we have enabled local debugging and identified any deprecation warnings in our project, it&apos;s time to install new dependencies in our project.&lt;/p&gt;
&lt;p&gt;To get the latest Django version, we use the following command.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install -U Django
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run the above command, the following output should appear in your terminal. We have now successfully updated Django to ver 4.2.1, asgiref to ver 3.6.0, and tzdata to ver 2023.3.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Requirement already satisfied: Django in c:\users\wanjamike\appdata\local\programs\python\python310\lib\site-packages (3.1.8)
Collecting Django
  Downloading Django-4.2.1-py3-none-any.whl (8.0 MB)
     &#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501;&#x2501; 8.0/8.0 MB 43.5 kB/s eta 0:00:00
Requirement already satisfied: sqlparse&amp;gt;=0.3.1 in c:\users\wanjamike\appdata\local\programs\python\python310\lib\site-packages (from Django) (0.4.1)
Collecting tzdata
  Using cached tzdata-2023.3-py2.py3-none-any.whl (341 kB)
Collecting asgiref&amp;lt;4,&amp;gt;=3.6.0
  Using cached asgiref-3.6.0-py3-none-any.whl (23 kB)
Installing collected packages: tzdata, asgiref, Django
  Attempting uninstall: asgiref
    Found existing installation: asgiref 3.3.1
    Uninstalling asgiref-3.3.1:
      Successfully uninstalled asgiref-3.3.1
  Attempting to uninstall: Django
    Found existing installation: Django 3.1.8
    Uninstalling Django-3.1.8:
      Successfully uninstalled Django-3.1.8
Successfully installed Django-4.2.1 asgiref-3.6.0 tzdata-2023.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can update the remaining dependencies in our project using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install -U django-filter djangorestframework djangorestframework-simplejwt pytz sqlparse
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Successfully installed django-filter-23.2 djangorestframework-3.14.0 djangorestframework-simplejwt-5.2.2 pytz-2023.3 sqlparse-0.4.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s generate a new &lt;code&gt;requirements.txt&lt;/code&gt; file and double-check that we have the latest libraries in our project.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip freeze &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you open the &lt;code&gt;requirements.txt&lt;/code&gt;, you should find all the latest dependencies, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;asgiref==3.6.0
Django==4.2.1
django-filter==23.2
djangorestframework==3.14.0
djangorestframework-simplejwt==5.2.2
pytz==2023.3
sqlparse==0.4.4
tzdata==2023.3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Check methods and functions&lt;/h3&gt;
&lt;p&gt;In most software updates, developers will try to change how certain functions or components work to improve performance, efficiency, &lt;a href=&quot;https://www.honeybadger.io/blog/reducing-your-python-apps-memory-footprint/&quot;&gt;memory usage&lt;/a&gt;, or convenience. Rather than fighting such changes, consider embracing and integrating them into your application to realize their benefits.&lt;/p&gt;
&lt;p&gt;Note that while this may be a time-consuming step, the benefits are worth it. You should go through the official documentation to understand new changes and learn how you can integrate them into your application.&lt;/p&gt;
&lt;p&gt;Let&apos;s review some of the changes in Django 4.2 that can help you in future upgrades.&lt;/p&gt;
&lt;p&gt;First, support for MariaDB 10.3, MySQL 5.7, and PostgreSQL 11 databases was dropped. If your application was using any of these database versions, consider upgrading. Make sure to back up your data before upgrading to avoid service disruptions.&lt;/p&gt;
&lt;p&gt;Second, Django 4.2 changed how we do data indexing. In the past, we indexed data using the statement below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;index_together = [[&amp;quot;rank&amp;quot;, &amp;quot;name&amp;quot;]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But now we must call the &lt;code&gt;Index&lt;/code&gt; method from the &lt;code&gt;models&lt;/code&gt; class, as demonstrated below.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;indexes = [models.Index(fields=[&amp;quot;rank&amp;quot;, &amp;quot;name&amp;quot;])]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Third, the &lt;code&gt;length_is&lt;/code&gt; template filter is now deprecated and has instead been replaced by the traditional &lt;code&gt;==&lt;/code&gt; operator.&lt;/p&gt;
&lt;p&gt;Don&apos;t do this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;{% if value|length_is:4 %}&#x2026;{% endif %}
{{ value|length_is:4 }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;{% if value|length == 4 %}&#x2026;{% endif %}
{% if value|length == 4 %}True{% else %}False{% endif %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Updating Python is important&lt;/h2&gt;
&lt;p&gt;Updates are a normal thing during software development. Therefore, it&apos;s best to learn how to update your app&apos;s Python version and integrate changes in our applications. Apart from improved security, software updates allow us to deal with bugs and ensure that our code is maintainable.&lt;/p&gt;
&lt;p&gt;When upgrading to a new version of Python, make sure to test things out in your local environment before pushing them to production. We don&apos;t want things to break and cause a poor user experience.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Errors in Python: types, causes, and examples</title>
    <link rel="alternate" href="https://www.honeybadger.io/blog/errors-in-python/"/>
    <id>https://www.honeybadger.io/blog/errors-in-python/</id>
    <published>2026-04-27T00:00:00+00:00</published>
    <updated>2026-04-27T00:00:00+00:00</updated>
    <author>
      <name>Aditya Raj</name>
    </author>
    <summary type="text">Errors in Python can arise from invalid syntax, unexpected issues during execution, system issues, or flaws in program logic. Learn about the different types of Python errors and practical ways to identify and avoid them to build more reliable programs.</summary>
    <content type="html">&lt;p&gt;Errors in Python are issues in a program that cause incorrect results or prevent proper execution. Some Python errors are loud and obvious, and your code barely gets started before it throws an error that tells you exactly what went wrong. Other errors are more subtle, allowing your Python program to run without complaints while silently producing incorrect results that only become apparent later. These differences become clearer when you group errors in Python based on how they occur and how they impact execution.&lt;/p&gt;
&lt;p&gt;For example, syntax errors get caught before the code even runs, and runtime errors blow up mid-execution. System-level errors have nothing to do with your code&#x2014;and they still stop execution&#x2014;while logical errors don&#x2019;t stop execution but produce incorrect results. Understanding the different types of errors and learning how to avoid them is essential for writing reliable and robust code. Let&apos;s look at the different types of errors in Python, their causes, and how to avoid them.&lt;/p&gt;
&lt;h2&gt;Different types of errors in Python&lt;/h2&gt;
&lt;p&gt;We can broadly categorize Python errors into four types, as shown in the following image:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/errors-in-python/errors-in-python.png&quot; alt=&quot;Diagram showing different types of errors in Python&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Syntax errors&lt;/strong&gt;: Syntax errors in Python occur due to invalid syntax, incorrect indentation, or typos. These errors are detected before the program is executed, while the Python interpreter parses the code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime errors&lt;/strong&gt;: In Python, runtime errors occur during execution when the program encounters an invalid operation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System-level errors&lt;/strong&gt;: System-level errors in Python are raised by the runtime environment or the operating system due to issues such as memory overflow or interruptions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logical errors&lt;/strong&gt;: Logical errors occur when the program&apos;s logic is incorrect, even though the code runs without errors, producing incorrect results.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&apos;s discuss all these Python error types one by one in detail with examples, starting with syntax errors.&lt;/p&gt;
&lt;h2&gt;Syntax errors in Python&lt;/h2&gt;
&lt;p&gt;Syntax errors are the errors caused by invalid code structure, typos, incorrect indentation, etc. For example, an &lt;code&gt;if&lt;/code&gt; block is defined in Python using the &lt;code&gt;if&lt;/code&gt; keyword, a boolean condition, and the &lt;code&gt;:&lt;/code&gt; character. If we skip &lt;code&gt;:&lt;/code&gt; while writing an &lt;code&gt;if&lt;/code&gt; block in Python, the code runs into &lt;code&gt;SyntaxError&lt;/code&gt; with the error message &lt;code&gt;SyntaxError: expected &apos;:&apos;&lt;/code&gt;, as shown in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;if x &amp;gt; 10
    print(&amp;quot;Honeybadger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code gives the following error message:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    if x &amp;gt; 10
             ^
SyntaxError: expected &apos;:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Syntax errors occur when the interpreter cannot parse the code because it violates Python&#x2019;s grammar rules. Let&apos;s discuss some of the common syntax errors in Python.&lt;/p&gt;
&lt;h3&gt;Indentation errors in Python&lt;/h3&gt;
&lt;p&gt;Python uses spaces and tabs for code indentation. The code will run into &lt;code&gt;IndentationError&lt;/code&gt; if a code block doesn&apos;t have correct indentation. For example, defining an &lt;code&gt;if&lt;/code&gt; block requires us to indent the lines in the &lt;code&gt;if&lt;/code&gt; block to the right by two/four spaces. If we don&apos;t indent the lines in the &lt;code&gt;if&lt;/code&gt; block, the code runs into &lt;code&gt;IndentationError&lt;/code&gt; with the error message &lt;code&gt;IndentationError: expected an indented block after &apos;if&apos; statement on line x&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;if x &amp;gt; 10:
print(&amp;quot;Honeybadger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The unindented if block gives the following error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 2
    print(&amp;quot;Honeybadger&amp;quot;)
    ^
IndentationError: expected an indented block after &apos;if&apos; statement on line 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, we need to indent the code block inside a function definition. Not doing so gives us an &lt;code&gt;IndentationError&lt;/code&gt; with the message &lt;code&gt;IndentationError: expected an indented block after function definition on line x&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello(name):
print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the print statement inside the &lt;code&gt;say_hello()&lt;/code&gt; function isn&apos;t indented to the right. Hence, the code gives the following error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 2
    print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;)
    ^
IndentationError: expected an indented block after function definition on line 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you indent a code block, it is important to keep the indentation the same for each statement in the code block. Otherwise, the program runs into &lt;code&gt;IndentationError&lt;/code&gt;. For example, if you indent the first statement of a code block by four spaces and the second statement by two spaces, the program will run into &lt;code&gt;IndentationError&lt;/code&gt; with the error message &lt;code&gt;IndentationError: unindent does not match any outer indentation level&lt;/code&gt;, as shown in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello(name):
    print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;)
  print(&amp;quot;Great seeing you here.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 3
    print(&amp;quot;Great seeing you here.&amp;quot;)
                                   ^
IndentationError: unindent does not match any outer indentation level
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, if you indent the first statement in a code block by two spaces and the second statement by four spaces, the program will run into &lt;code&gt;IndentationError&lt;/code&gt; with the message &lt;code&gt;IndentationError: unexpected indent&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello(name):
  print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;)
    print(&amp;quot;Great seeing you here.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 3
    print(&amp;quot;Great seeing you here.&amp;quot;)
IndentationError: unexpected indent
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tab error&lt;/h3&gt;
&lt;p&gt;TabError is a specific indentation error caused by mixing tabs and spaces to indent code blocks. For instance, you can indent a statement by the same distance using four spaces or one tab. Visually, it looks the same. However, if you indent a statement in a code block using a tab and another using four spaces, the program will run into &lt;code&gt;TabError&lt;/code&gt; with the error message &lt;code&gt;TabError: inconsistent use of tabs and spaces in indentation&lt;/code&gt;, as shown in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello(name):
    print(f&amp;quot;Hi {name}, you are at Honeybadger&amp;quot;) # Indentation using Tab
    print(&amp;quot;Great seeing you here.&amp;quot;)  # Indentation using four spaces
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 3
    print(&amp;quot;Great seeing you here.&amp;quot;)  # Indentation using four spaces
TabError: inconsistent use of tabs and spaces in indentation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python 3 explicitly disallows mixing tabs and spaces for indentation in a way that makes the meaning ambiguous, and you should always avoid it.&lt;/p&gt;
&lt;h3&gt;Unclosed strings/ brackets/ parentheses&lt;/h3&gt;
&lt;p&gt;A Python program runs into a &lt;code&gt;SyntaxError&lt;/code&gt; if you don&apos;t close a string, parentheses, or a bracket. For example, if you don&apos;t close a string, the program runs into  &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: unterminated string literal&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;if x &amp;gt; 10:
    print(&amp;quot;Honeybadger)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the closing &lt;code&gt;&amp;quot;&lt;/code&gt; is missing in the &lt;code&gt;&amp;quot;Honeybadger&lt;/code&gt; string. Due to this, the program runs into &lt;code&gt;SyntaxError&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 2
    print(&amp;quot;Honeybadger)
          ^
SyntaxError: unterminated string literal (detected at line 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, if we forget the closing parenthesis while calling a function or defining a tuple, the program runs into &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: &apos;(&apos; was never closed&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;print(&amp;quot;Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    print(&amp;quot;Honeybadger&amp;quot;
         ^
SyntaxError: &apos;(&apos; was never closed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just like parentheses, if we miss the closing bracket while defining a list, the program runs into &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: &apos;[&apos; was never closed&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_list = [1, 2, 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    my_list = [1, 2, 3
              ^
SyntaxError: &apos;[&apos; was never closed
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Invalid assignment errors&lt;/h3&gt;
&lt;p&gt;Invalid assignment errors are mostly caused by assigning values to literals or function calls. For example, if we assign the value &lt;code&gt;&amp;quot;Honeybadger&amp;quot;&lt;/code&gt; to a string literal &lt;code&gt;&amp;quot;name&amp;quot;&lt;/code&gt;, the program throws a &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: cannot assign to literal here. Maybe you meant &apos;==&apos; instead of &apos;=&apos;?&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;&amp;quot;name&amp;quot; = &amp;quot;Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    &amp;quot;name&amp;quot; = &amp;quot;Honeybadger&amp;quot;
    ^^^^^^
SyntaxError: cannot assign to literal here. Maybe you meant &apos;==&apos; instead of &apos;=&apos;?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, if we assign a value to a Python keyword, the program runs into a &lt;code&gt;SyntaxError&lt;/code&gt;. For example, assigning the value &lt;code&gt;Honeybadger&lt;/code&gt; to a variable &lt;code&gt;class&lt;/code&gt; results in a &lt;code&gt;SyntaxError&lt;/code&gt; with the message &lt;code&gt;SyntaxError: invalid syntax&lt;/code&gt;, as &lt;code&gt;class&lt;/code&gt; is a Python keyword.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;class = &amp;quot;Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 1
    class = &amp;quot;Honeybadger&amp;quot;
          ^
SyntaxError: invalid syntax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An assignment error also occurs when we miss a &lt;code&gt;=&lt;/code&gt; character while comparing values using the equality operator. For example, if we use &lt;code&gt;=&lt;/code&gt; instead of &lt;code&gt;==&lt;/code&gt; to compare two values, the program runs into &lt;code&gt;SyntaxError&lt;/code&gt; with the error message &lt;code&gt;SyntaxError: invalid syntax. Maybe you meant &apos;==&apos; or &apos;:=&apos; instead of &apos;=&apos;?&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;name = &amp;quot;Honeybadger&amp;quot;
input_string = &amp;quot;Honeybadger&amp;quot;
if name = input_string:
  print(name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;  File &amp;quot;/path/to/code.py&amp;quot;, line 3
    if name = input_string:
       ^^^^^^^^^^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant &apos;==&apos; or &apos;:=&apos; instead of &apos;=&apos;?
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How to avoid syntax errors in Python?&lt;/h3&gt;
&lt;p&gt;Syntax errors occur due to incorrect indentation, mismatched delimiters, missing punctuation, invalid variable names, or incorrect operators. You can avoid syntax errors using the following best practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Always add the required colon &lt;code&gt;:&lt;/code&gt; after block statements like &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;, &lt;code&gt;while&lt;/code&gt;, &lt;code&gt;def&lt;/code&gt;, and &lt;code&gt;class&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Avoid using reserved keywords such as &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;, &lt;code&gt;if&lt;/code&gt;, or &lt;code&gt;return&lt;/code&gt; as variable names.&lt;/li&gt;
&lt;li&gt;Always close all the parentheses &lt;code&gt;()&lt;/code&gt;, brackets &lt;code&gt;[]&lt;/code&gt;, and braces &lt;code&gt;{}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Close all string literals with matching quotation marks &lt;code&gt;&apos;&lt;/code&gt; or &lt;code&gt;&amp;quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Follow Python syntax rules and ensure statements are written in the correct format.&lt;/li&gt;
&lt;li&gt;Maintain consistent indentation, preferably using 4 spaces per indentation level, and do not mix tabs and spaces for indentation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In addition to the above practices, you can use IDEs or code editors such as PyCharm, Spyder, or VS Code that provide syntax highlighting and linting to detect syntax errors early.&lt;/p&gt;
&lt;h2&gt;Runtime errors in Python&lt;/h2&gt;
&lt;p&gt;Runtime errors occur in a Python program after it passes the syntax check and starts executing, when something goes wrong. Examples of runtime errors in Python include &lt;code&gt;ZeroDivisionError&lt;/code&gt;, &lt;code&gt;NameError&lt;/code&gt;, &lt;code&gt;TypeError&lt;/code&gt;, and &lt;code&gt;ValueError&lt;/code&gt;. Let&#x2019;s discuss the different runtime errors, their causes, and ways to avoid them.&lt;/p&gt;
&lt;h3&gt;Zero division error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ZeroDivisionError&lt;/code&gt; exception is one of the most common arithmetic errors that occurs if the denominator of a division operation is zero.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;x = 10 / 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we are dividing 10 by 0. Hence, the program runs into &lt;code&gt;ZeroDivisionError&lt;/code&gt; with the error message &lt;code&gt;ZeroDivisionError: division by zero&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    x = 10 / 0
ZeroDivisionError: division by zero
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Name error&lt;/h3&gt;
&lt;p&gt;The NameError exception is a runtime error that occurs when a variable is referenced before assignment.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;y = x / 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we tried to divide &lt;code&gt;x&lt;/code&gt; by 10 without defining &lt;code&gt;x&lt;/code&gt; or assigning it a value. Hence, the variable name &lt;code&gt;x&lt;/code&gt;  isn&apos;t present in the scope of the program, and the program runs into a &lt;code&gt;NameError&lt;/code&gt; exception with the error message &lt;code&gt;NameError: name &apos;x&apos; is not defined&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    y = x / 10
NameError: name &apos;x&apos; is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;NameError&lt;/code&gt; exception also occurs if you use a variable first and define it later in the program. For example, consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;print(greeting)
greeting = &amp;quot;Hi, you are at Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we have referenced the variable &lt;code&gt;greeting&lt;/code&gt; and later assigned it a value. Hence, the program throws a &lt;code&gt;NameError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    print(greeting)
NameError: name &apos;greeting&apos; is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Unbound local error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;UnboundLocalError&lt;/code&gt; exception occurs when a local variable is referenced before assignment, in a function or method. For instance, consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello():
    print(greeting)
    greeting = &amp;quot;Hi, you are at Honeybadger&amp;quot;
    
say_hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we have referenced the variable &lt;code&gt;greeting&lt;/code&gt; before assigning it a value in the &lt;code&gt;say_hello()&lt;/code&gt; function. When we call the &lt;code&gt;say_hello()&lt;/code&gt; function, the program raises the &lt;code&gt;UnboundLocalError&lt;/code&gt; exception with the message &lt;code&gt;UnboundLocalError: local variable &apos;greeting&apos; referenced before assignment&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 5, in &amp;lt;module&amp;gt;
    say_hello()
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in say_hello
    print(greeting)
UnboundLocalError: local variable &apos;greeting&apos; referenced before assignment
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, if we hadn&apos;t assigned any value to the variable after the print statement, the program would have run into a &lt;code&gt;NameError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def say_hello():
    print(greeting)
    print(&amp;quot;Hi, you are at Honeybadger&amp;quot;)

say_hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 5, in &amp;lt;module&amp;gt;
    say_hello()
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in say_hello
    print(greeting)
NameError: name &apos;greeting&apos; is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Type error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;TypeError&lt;/code&gt; exceptions in Python occur when an operation is applied to a value or variable of an incompatible data type. For example, adding an integer and a string results in a &lt;code&gt;TypeError&lt;/code&gt; exception with the error message &lt;code&gt;TypeError: unsupported operand type(s) for +: &apos;int&apos; and &apos;str&apos;&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;x = 10 + &amp;quot;Honeybadger&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    x = 10 + &amp;quot;Honeybadger&amp;quot;
TypeError: unsupported operand type(s) for +: &apos;int&apos; and &apos;str&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, calling a non-callable object or iterating on a non-iterable object also results in a &lt;code&gt;TypeError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;name = &amp;quot;Honeybadger&amp;quot;
name()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    name()
TypeError: &apos;str&apos; object is not callable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we defined a string variable &lt;code&gt;name&lt;/code&gt; and tried to use it as a function in the second line. Hence, the program throws a &lt;code&gt;TypeError&lt;/code&gt; exception with the message &lt;code&gt;TypeError: &apos;str&apos; object is not callable&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Value error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ValueError&lt;/code&gt; exception occurs in a Python program when we use an input or a variable with the correct data type but an inappropriate value. For instance, the &lt;code&gt;int()&lt;/code&gt; function converts a string to an integer. If the string passed to the &lt;code&gt;int()&lt;/code&gt; function cannot be converted into an integer, the program runs into a &lt;code&gt;ValueError&lt;/code&gt; exception, as shown in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;x = int(&amp;quot;10&amp;quot;)
y = x + int(&amp;quot;Honeybadger&amp;quot;)
print(y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the statement &lt;code&gt;x=int(&amp;quot;10&amp;quot;)&lt;/code&gt; executes successfully because &lt;code&gt;&amp;quot;10&amp;quot;&lt;/code&gt; is successfully converted into an integer. However, the string &lt;code&gt;&amp;quot;Honeybadger&amp;quot;&lt;/code&gt; cannot be converted to an integer. Hence, the second line of the code raises a &lt;code&gt;ValueError&lt;/code&gt; exception with the error message &lt;code&gt;ValueError: invalid literal for int() with base 10: &apos;Honeybadger&apos;&lt;/code&gt;, as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    y = x + int(&amp;quot;Honeybadger&amp;quot;)
ValueError: invalid literal for int() with base 10: &apos;Honeybadger&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, square roots aren&apos;t defined for negative numbers. Hence, passing a negative number to the &lt;code&gt;math.sqrt()&lt;/code&gt; function results in a &lt;code&gt;ValueError&lt;/code&gt; exception due to an inappropriate value.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import math
x = math.sqrt(-10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    x = math.sqrt(-10)
ValueError: math domain error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, -10 has the correct integer data type that the &lt;code&gt;sqrt()&lt;/code&gt; function requires. However, it is an invalid value, and we get a &lt;code&gt;ValueError&lt;/code&gt; exception with the message &lt;code&gt;ValueError: math domain error&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Index error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;IndexError&lt;/code&gt; exception occurs in a Python program when we try to access an element at an index that doesn&apos;t exist in an iterable object like a string, list, or tuple. For example, if a list has six elements and we try to access the element at index 6 (the seventh element), the program runs into an &lt;code&gt;IndexError&lt;/code&gt; exception with the message &lt;code&gt;IndexError: list index out of range&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_list = [1, 2, 3, 4, 5, 6]
print(my_list[6])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    print(my_list[6])
IndexError: list index out of range
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarly, if we try to access an element at a non-existent index in a string, the program runs into an &lt;code&gt;IndexError&lt;/code&gt; exception with the message &lt;code&gt;IndexError: string index out of range&lt;/code&gt;, as shown in the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;name = &amp;quot;Honeybadger&amp;quot;
print(name[20])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we tried to access the character at index 20 in the string. However, the string is eleven characters long. Hence, the program raises an &lt;code&gt;IndexError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    print(name[20])
IndexError: string index out of range
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key error&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;KeyError&lt;/code&gt; exceptions occur when we try to access a non-existent key in a Python dictionary. For instance, the dictionary in the following code has keys &lt;code&gt;&amp;quot;a&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;b&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;c&amp;quot;&lt;/code&gt;, and &lt;code&gt;&amp;quot;d&amp;quot;&lt;/code&gt;. When we try to fetch a value with the key &lt;code&gt;&amp;quot;e&amp;quot;&lt;/code&gt;, the program runs into a &lt;code&gt;KeyError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_dict = {&amp;quot;a&amp;quot;: 1, &amp;quot;b&amp;quot;: 2, &amp;quot;c&amp;quot;: 3, &amp;quot;d&amp;quot;: 4}
print(my_dict[&amp;quot;e&amp;quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    print(my_dict[&amp;quot;e&amp;quot;])
KeyError: &apos;e&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Module not found error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ModuleNotFoundError&lt;/code&gt; occurs when we try to import a module that hasn&apos;t already been installed or downloaded to the Python module search path.&lt;/p&gt;
&lt;p&gt;For example, suppose you want to use &lt;a href=&quot;https://docs.honeybadger.io/lib/python/integrations/other/&quot;&gt;Honeybadger for error monitoring in a Python application&lt;/a&gt;. However, if you don&apos;t &lt;a href=&quot;https://pypi.org/project/honeybadger/&quot;&gt;install honeybadger using pip&lt;/a&gt; and start directly by importing the &lt;code&gt;honeybadger&lt;/code&gt; module into your code, the program will run into &lt;code&gt;ModuleNotFoundError&lt;/code&gt; with the message &lt;code&gt;ModuleNotFoundError: No module named &apos;honeybadger&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import honeybadger
print(&amp;quot;You are at Honeybadger&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    import honeybadger
ModuleNotFoundError: No module named &apos;honeybadger&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ModuleNotFoundError&lt;/code&gt; is a specific type of &lt;code&gt;ImportError&lt;/code&gt; that occurs when Python cannot find the module file being imported.&lt;/p&gt;
&lt;h3&gt;Import errors in Python&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ImportError&lt;/code&gt; is a more general exception that can occur for various reasons during the import process, even when the module file exists but cannot be imported successfully due to dependency requirements or other issues. If a module exists but raises an exception while being imported, Python raises &lt;code&gt;ImportError&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For instance, if we try to import a non-existent function from the &lt;code&gt;honeybadger&lt;/code&gt; module after installing it, the program runs into an &lt;code&gt;ImportError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;from honeybadger import nonexistingfunction
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we tried to import &lt;code&gt;nonexistingfunction&lt;/code&gt; from the &lt;code&gt;honeybadger&lt;/code&gt; module. Hence, the program raises an &lt;code&gt;ImportError&lt;/code&gt; with the message &lt;code&gt;ImportError: cannot import name &apos;nonexistingfunction&apos; from &apos;honeybadger&apos;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    from honeybadger import nonexistingfunction
ImportError: cannot import name &apos;nonexistingfunction&apos; from &apos;honeybadger&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Attribute error&lt;/h3&gt;
&lt;p&gt;In Python, every object has a set of associated attributes, i.e., field names and methods. For example, a Python list has the &lt;code&gt;append()&lt;/code&gt; method that we use to add new values to a list. However, a tuple, an integer, a string, or a floating-point value doesn&apos;t have the &lt;code&gt;append()&lt;/code&gt; method. Hence, if we invoke the &lt;code&gt;append()&lt;/code&gt; method on a tuple, the program raises an &lt;code&gt;AttributeError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_tuple = (1, 2, 3, 4, 5)
my_tuple.append(6)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, we have used the &lt;code&gt;append()&lt;/code&gt; method on a tuple. Hence, the program throws an &lt;code&gt;AttributeError&lt;/code&gt; exception with the message &lt;code&gt;AttributeError: &apos;tuple&apos; object has no attribute &apos;append&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 2, in &amp;lt;module&amp;gt;
    my_tuple.append(6)
AttributeError: &apos;tuple&apos; object has no attribute &apos;append&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Memory error&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MemoryError&lt;/code&gt; in Python is a built-in exception that occurs when a program fails to allocate memory for a Python object. It usually occurs when handling extremely large datasets, constructing oversized data structures, or running inefficient code that leads to excessive memory consumption or memory leaks.&lt;/p&gt;
&lt;p&gt;For example, creating a huge list having ten billion elements can lead to a &lt;code&gt;MemoryError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;my_list = [10] * (10**10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    my_list = [10] * (10**10)
MemoryError
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While &lt;code&gt;MemoryError&lt;/code&gt; is a runtime error, it often indicates that the program has exceeded a system limit, such as running out of RAM or exceeding the operating system&apos;s address space limit.&lt;/p&gt;
&lt;h3&gt;Recursion error&lt;/h3&gt;
&lt;p&gt;A recursion error is a runtime error that occurs when the recursion depth exceeds the limit of 1000 recursive calls. The &lt;code&gt;RecursionError&lt;/code&gt; exception occurs if we forget to add a base case or a terminating condition while defining a recursive function. For instance, consider the following &lt;code&gt;increment_till_hundred()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;def increment_till_hundred(x):
    x += 1
    print(x)
    increment_till_hundred(x)
    
increment_till_hundred(80)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;code&gt;increment_till_hundred()&lt;/code&gt; function, we haven&apos;t defined any termination condition if the value of &lt;code&gt;x&lt;/code&gt; reaches 100. Hence, the function keeps making the recursive call, exceeding the limit of 1000 recursive calls, and the program runs into &lt;code&gt;RecursionError&lt;/code&gt; with the message &lt;code&gt;RecursionError: maximum recursion depth exceeded while calling a Python object&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 6, in &amp;lt;module&amp;gt;
    increment_till_hundred(80)
  File &amp;quot;/path/to/code.py&amp;quot;, line 4, in increment_till_hundred
    increment_till_hundred(x)
  [Previous line repeated 994 more times]
  File &amp;quot;/path/to/code.py&amp;quot;, line 3, in increment_till_hundred
    print(x)
RecursionError: maximum recursion depth exceeded while calling a Python object
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How to avoid runtime errors in Python?&lt;/h3&gt;
&lt;p&gt;Runtime errors are difficult to detect because they do not prevent the program from starting execution, unlike syntax errors. Hence, the program runs normally at first, and the error may only appear later when the line containing the problematic code is executed. To avoid runtime errors, you can use the following best practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Always validate the input data before processing to ensure it has the correct data type, format, and range.&lt;/li&gt;
&lt;li&gt;Always &lt;a href=&quot;https://www.honeybadger.io/blog/a-guide-to-exception-handling-in-python/&quot;&gt;implement exception handling in your Python code&lt;/a&gt; to catch and handle runtime errors gracefully, rather than crashing the program.&lt;/li&gt;
&lt;li&gt;Perform type checking using the &lt;code&gt;isinstance()&lt;/code&gt; function or convert the data type of values using &lt;code&gt;int()&lt;/code&gt;, &lt;code&gt;float()&lt;/code&gt;, and &lt;code&gt;str()&lt;/code&gt; functions before applying operations on values.&lt;/li&gt;
&lt;li&gt;Maintain a properly configured runtime environment, ensuring all required modules, dependencies, and system libraries are correctly installed.&lt;/li&gt;
&lt;li&gt;Test the code with different edge cases to identify potential runtime failures early.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Apart from the above practices, always write modular, well-structured code so you can easily isolate and debug errors if they occur.&lt;/p&gt;
&lt;h2&gt;System-level errors in Python&lt;/h2&gt;
&lt;p&gt;System-level errors occur in a Python program when it encounters issues such as I/O failures, memory overflows, or connection errors. All the system-level errors in Python are raised using the &lt;code&gt;OSError&lt;/code&gt; exception or its subclasses.  Let&apos;s discuss the different system-level errors in Python and how to avoid them.&lt;/p&gt;
&lt;h3&gt;File not found errors in Python&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;FileNotFoundError&lt;/code&gt; exception occurs when we try to read a non-existent file. For example, suppose that we want to read a text file named &lt;code&gt;sampletextfile.txt&lt;/code&gt; using the &lt;code&gt;open()&lt;/code&gt; function. If the file doesn&apos;t exist, the program runs into &lt;code&gt;FileNotFoundError&lt;/code&gt; with the message &lt;code&gt;FileNotFoundError: [Errno 2] No such file or directory: &apos;sampletextfile.txt&apos;&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;file = open(&amp;quot;sampletextfile.txt&amp;quot;, &amp;quot;r&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    file = open(&amp;quot;sampletextfile.txt&amp;quot;, &amp;quot;r&amp;quot;)
FileNotFoundError: [Errno 2] No such file or directory: &apos;sampletextfile.txt&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Permission error&lt;/h3&gt;
&lt;p&gt;If a file exists and we don&apos;t have permission to read or modify it, the program raises a &lt;code&gt;PermissionError&lt;/code&gt; exception. For example, suppose that we have a file &lt;code&gt;samplefile.txt&lt;/code&gt; with only read access, as shown in the image:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www-files.honeybadger.io/posts/errors-in-python/samplefile_permissions.png&quot; alt=&quot;Image showing permissions for samplefile.txt&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now, if we try to open the file in &lt;code&gt;append&lt;/code&gt; mode and modify it, the program raises a &lt;code&gt;PermissionError&lt;/code&gt; exception with the message &lt;code&gt;PermissionError: [Errno 13] Permission denied: &apos;samplefile.txt&apos;&lt;/code&gt;. However, opening the file in read mode will not cause any issues, as we have permission to read it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;file = open(&amp;quot;samplefile.txt&amp;quot;, &amp;quot;a&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    file = open(&amp;quot;samplefile.txt&amp;quot;, &amp;quot;a&amp;quot;)
PermissionError: [Errno 13] Permission denied: &apos;samplefile.txt&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Is a directory error&lt;/h3&gt;
&lt;p&gt;We can open files using the &lt;code&gt;open()&lt;/code&gt; function in Python. However, if you try to open a directory using the &lt;code&gt;open()&lt;/code&gt; function, the program raises the &lt;code&gt;IsADirectoryError&lt;/code&gt; exception.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;file = open(&amp;quot;/path/to/directory&amp;quot;, &amp;quot;r&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, &lt;code&gt;/path/to&lt;/code&gt; is a directory. Hence, the program throws an &lt;code&gt;IsADirectoryError&lt;/code&gt; exception with the message &lt;code&gt;IsADirectoryError: [Errno 21] Is a directory&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 1, in &amp;lt;module&amp;gt;
    file = open(&amp;quot;/path/to/directory&amp;quot;, &amp;quot;r&amp;quot;)
IsADirectoryError: [Errno 21] Is a directory: &apos;/path/to/directory&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Connection error&lt;/h3&gt;
&lt;p&gt;In Python, the &lt;code&gt;ConnectionError&lt;/code&gt; exception occurs due to network issues such as a lost internet connection, DNS errors, server downtime, or when the client fails to establish a connection to the server within a specified time limit. For instance, using the &lt;code&gt;requests&lt;/code&gt; module to make an API call without connecting the system to the network results in the &lt;code&gt;ConnectionError&lt;/code&gt; exception, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import requests
response = requests.get(&apos;https://jsonplaceholder.typicode.com/todos/1&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 4, in &amp;lt;module&amp;gt;
    response = requests.get(&apos;https://jsonplaceholder.typicode.com/todos/1&apos;)
  .
  .
  File &amp;quot;/home/user/.local/lib/python3.10/site-packages/requests/adapters.py&amp;quot;, line 700, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPSConnectionPool(host=&apos;jsonplaceholder.typicode.com&apos;, port=443): Max retries exceeded with url: /todos/1 (Caused by NameResolutionError(&amp;quot;&amp;lt;urllib3.connection.HTTPSConnection object at 0x7400d45a6c80&amp;gt;: Failed to resolve &apos;jsonplaceholder.typicode.com&apos; ([Errno -3] Temporary failure in name resolution)&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Connection refused error&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;ConnectionRefusedError&lt;/code&gt; is a subclass of &lt;code&gt;ConnectionError&lt;/code&gt;, which specifically indicates that a connection attempt was explicitly refused by the remote host. It occurs due to an incorrect IP address or port number, firewall blocking, or if the server is not running or has reached its maximum capacity for pending connections. For example, consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((&amp;quot;localhost&amp;quot;, 9999))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have not run any application on port 9999. Hence, when the Python program tries to connect to the port, the program runs into &lt;code&gt;ConnectionRefusedError&lt;/code&gt; with the message &lt;code&gt;ConnectionRefusedError: [Errno 111] Connection refused&lt;/code&gt;, as shown below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Traceback (most recent call last):
  File &amp;quot;/path/to/code.py&amp;quot;, line 3, in &amp;lt;module&amp;gt;
    s.connect((&amp;quot;localhost&amp;quot;, 9999))
ConnectionRefusedError: [Errno 111] Connection refused
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How to avoid system-level errors in Python?&lt;/h3&gt;
&lt;p&gt;System-level errors in Python are often caused by missing files, insufficient permissions, memory limitations, or network failures. Although we cannot always prevent them, we can minimize system-level errors through careful resource management, validation checks, and proper exception handling. You can use the following practices to avoid system-level errors in Python.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Always check whether files and directories exist before performing file operations.&lt;/li&gt;
&lt;li&gt;Check access permissions to ensure the program has the required read, write, and execute permissions for files and directories.&lt;/li&gt;
&lt;li&gt;Manage memory efficiently by avoiding extremely large data structures and using generators or batch processing for large datasets.&lt;/li&gt;
&lt;li&gt;Use context managers (&lt;code&gt;with&lt;/code&gt; statement) when working with files, sockets, or other resources to ensure they are automatically closed after use.&lt;/li&gt;
&lt;li&gt;Ensure required system resources, such as disk space, memory, and network connectivity, are available.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best way to avoid system-level errors is to anticipate potential failures and write defensive code that validates resources and handles exceptions gracefully, since these are mostly caused by environmental or resource constraints.&lt;/p&gt;
&lt;h2&gt;Logical errors in Python&lt;/h2&gt;
&lt;p&gt;A logical or semantic error in Python occurs when a program runs without crashing or raising exceptions but produces incorrect or unintended results due to flawed code logic. Unlike syntax errors or runtime errors, Python cannot detect logical errors automatically because the code is syntactically valid and executes successfully. Logical errors occur due to incorrect algorithms, wrong conditions, faulty calculations, or mistaken assumptions in program logic.&lt;/p&gt;
&lt;p&gt;For example, consider that you are writing a Python application to determine the voting eligibility of a person based on their age. It is given that a person aged 18 or older is eligible to vote. Now consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;if age &amp;gt; 18:
    print(&amp;quot;Eligible to vote&amp;quot;)
else:
    print(&amp;quot;Not eligible&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code executes successfully. But it produces incorrect output for people aged 18 due to an incorrect condition. Hence, we should have used the &lt;code&gt;&amp;gt;=&lt;/code&gt; operator instead of the &lt;code&gt;&amp;gt;&lt;/code&gt; operator in the if block.&lt;/p&gt;
&lt;p&gt;Logical errors can also occur due to incorrect use of logical operators. For example, suppose we need to determine whether a person is of working age, defined as 18 to 60 years old, inclusive. Now, consider the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;age = 70
if age &amp;gt;= 18 or age &amp;lt;= 60:
    print(&amp;quot;Working age&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;Working age
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code prints &lt;code&gt;&amp;quot;Working age&amp;quot;&lt;/code&gt; even if the person is over 60 years old because the first condition is True, and the &lt;code&gt;or&lt;/code&gt; operator evaluates to True if either operand is True. Hence, we should have used the &lt;code&gt;and&lt;/code&gt; operator rather than the &lt;code&gt;or&lt;/code&gt; operator to get the correct output.&lt;/p&gt;
&lt;p&gt;Logical errors are much harder to detect because the Python application does not run into exceptions. The output may appear reasonable but will still be incorrect. We can detect and avoid logical errors through &lt;a href=&quot;https://www.honeybadger.io/blog/beginners-guide-to-software-testing-in-python/&quot;&gt;unit testing&lt;/a&gt;, debugging, and code review, ensuring there are no flaws in the code.&lt;/p&gt;
&lt;h2&gt;Know your errors before your users do&lt;/h2&gt;
&lt;p&gt;No matter how experienced you get, errors in an application never fully disappear. What changes is how quickly you recognize them, how calmly you respond, and how efficiently you solve the errors. In this article, we discussed the different syntax errors, runtime errors, system-level errors, and logical errors in Python and how to avoid them. While we cannot eliminate errors entirely, we can significantly reduce them by following &lt;a href=&quot;https://www.honeybadger.io/blog/fastapi-error-handling/&quot;&gt;error-handling best practices&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is important to recognize that not every error can be caught during development. Certain bugs only emerge in production, often triggered by specific user actions in unpredictable sequences that may not be covered by testing. When this occurs, users may encounter an error before the development team discovers it. This is where error tracking and application monitoring become essential.&lt;/p&gt;
&lt;p&gt;Honeybadger provides error tracking, logging, uptime monitoring, and performance monitoring under one roof. &lt;a href=&quot;https://www.honeybadger.io/plans/&quot;&gt;Sign up for a free trial of Honeybadger&lt;/a&gt; to monitor your applications by combining error tracking, logging, and uptime monitoring, so you always know the state of your application and can catch errors before they snowball.&lt;/p&gt;
</content>
  </entry>
</feed>