A runtime error is an error that passed the parser, passed the type checker, and failed at execution. The compiler or interpreter was happy with the code; the runtime was not. That is the whole distinction. The diagnostic is to look at the stack trace, the inputs that were actually passed, and the most recent change to the code path that failed. Three places, five seconds, most of the time you know what happened.
The reason runtime errors feel mysterious is that the code “worked” in some environment — locally, in tests, in staging — and is failing in another. Production. The customer’s browser. The third-party API. The place where the inputs are not the ones the code was tested against. Runtime errors are almost always about inputs that did not match the assumption, or state that the code did not handle, or external systems that did not behave the way the code expected.
The five-second diagnostic works because runtime errors, unlike logic bugs, leave fingerprints. The stack trace names the line. The log line names the input. The git log names the change. The triangle of evidence is usually enough to find the cause without a debugger, without a reproducer, and without guessing.
Table of contents
- What counts as a runtime error
- The four categories of runtime error
- The five-second diagnostic
- The production-only runtime errors
- The catches that turn runtime errors into 500s
- The observability pattern
- The post-mortem pattern
- How this fits the rest of the stack
- FAQ
What counts as a runtime error
The category is the set of errors that only surface when the program runs. Syntax errors do not count — those are caught at parse time. Type errors do not count if the language is statically typed and the type checker ran — those are caught at compile time. Logic bugs do not count — the code runs, returns the wrong value, and never throws.
A runtime error has three properties:
- The code parsed.
- The code type-checked (in statically typed languages) or linted clean (in dynamically typed languages where the team runs a linter).
- The code threw an exception, returned an error code, or otherwise failed at execution.
The first two are why the bug is interesting — the tooling said the code was fine. The third is why the bug is fixable — there is an exception, a stack trace, a log line, something concrete to look at.
A useful sanity check: if the error can be reproduced by running the code with a specific input, it is a runtime error. If the error can only be reproduced by reading the code and noticing a logic flaw, it is a logic bug. The first kind has a stack trace; the second kind does not.
The four categories of runtime error
In most languages, runtime errors fall into four buckets, and knowing the bucket tells you what to look for.
Type errors (TypeError in JS/Python, ClassCastException in Java, panic from a type assertion in Go). The code tried to use a value as the wrong type. null.foo in a null-safe language, undefined.length in JavaScript, a string where a number was expected in a dynamically typed language. The fix is almost always an input validation that runs before the type-using line, or a null check, or a type guard.
Reference errors (ReferenceError in JS, NameError in Python, NullPointerException in Java). The code tried to access something that does not exist. A variable that was never declared, a property on a null object, a key that is not in the dictionary, an index that is out of range. The fix is a guard before the access, or a default value, or a different data structure that makes the missing case impossible.
Range errors (RangeError, IndexOutOfBoundsException, OverflowError). The code tried to use a value that is out of the valid range. An array index that is negative or past the end, a number that overflows the integer, a recursion that goes too deep. The fix is a bounds check, a bigger data type, or a loop that does not recurse.
Custom exceptions (anything the code itself throws). Domain-specific errors that the application defines. “User not found,” “Payment failed,” “Inventory depleted.” These are runtime errors by definition — they are thrown at execution — and they usually have a more useful message than the language-level categories.
The categories are useful because the search-and-replace fix is different for each. A TypeError is almost never fixed by adding a try/catch; it is fixed by validating the input. A RangeError is fixed by a bounds check. A custom exception is fixed by handling the specific case the code is raising for. Adding a try/catch without understanding the category is how runtime errors get hidden instead of fixed.
The five-second diagnostic
Three places, in this order.
The stack trace. The top frame is the line that threw. The frames above it are the call chain. In 90% of runtime errors, the top frame and the message are enough to identify the problem without reading the rest of the trace. “Cannot read property ‘name’ of undefined” at user.profile.name means the profile is undefined; the line of code is the question, the rest of the trace is the context.
The inputs. What was actually passed to the function? The error happened with a specific input that did not match the assumption. The log line for the failing request, the database record that was loaded, the request body that came in. The discrepancy between the actual input and the assumed input is the cause. The fix is either to make the code handle the actual input, or to add a check upstream that rejects the input before the code sees it.
The recent change. git log --oneline -10 on the file that contains the failing line. git blame on the line itself. The runtime error was not there before, it is there now, and the most recent change to that code path is where the regression lives. This is also the place where the team finds the kind of bug that “worked locally” — the local code, the local data, the local environment, did not match the production code, data, or environment.
The three places, in that order, take five seconds. If they do not produce a hypothesis, the next step is to reproduce the error in a local environment with the same inputs, which usually takes another five minutes. The five-second diagnostic is the answer to the “where do I even start” question, not the answer to every bug.
The production-only runtime errors
A class of runtime errors does not happen in development. They only happen in production. Knowing the class saves an hour of “but it works on my machine.”
Environment variable missing. The dev .env has the variable, the production deploy does not. The error is a None or undefined deep in the call stack, where the code expected a non-empty string. The fix is to validate required env vars at startup and fail the deploy with a clear error, not to let the service start and crash on the first request.
Database is down or unreachable. The dev machine has a local database, the production database is on a separate host with separate network rules. The first request after a network blip throws a connection error. The fix is a connection pool with a health check, a retry policy with backoff, and a circuit breaker that fails fast instead of cascading.
Third-party API rate-limited or returning 5xx. The dev environment calls the API with a personal token and never hits a limit. The production environment calls with a service token, hits a limit, and the code does not handle the 429. The fix is rate-limit-aware client code, exponential backoff, and a graceful degradation path that serves a stale or empty result instead of a 500.
Out of memory. The dev machine has 16 GB of RAM, the production container has 512 MB. A function that builds a large list in memory works locally and OOMs in production. The fix is to size the runtime for the actual data, or to stream the data instead of buffering it, or to chunk the processing.
The pattern across all four: the difference between dev and prod is the trigger, and the production-only runtime error is the symptom. The fix is to make the dev environment match the production environment, or to make the code robust to the difference. The run logs that show the deploy and the runtime in the same place make this class of bug obvious the first time it happens, instead of the third.
The catches that turn runtime errors into 500s
A runtime error becomes a 500 when the code that catches it (or does not catch it) returns a generic error to the caller. Three patterns to look for.
The empty catch block. try { ... } catch (e) {} in JavaScript, except: pass in Python. The code threw, the code swallowed the throw, and the rest of the function ran with broken state. The next thing that uses the broken state is the thing that returns a 500. The fix is to log the caught error, mark the state as broken, and return a clear error to the caller.
The async error that nobody awaited. In JavaScript, a Promise that rejects without a .catch() becomes an unhandled rejection. The runtime logs the warning, the request that triggered the Promise returns whatever it was going to return, and the rejection is invisible. In Python, an asyncio.create_task() that is never awaited has the same problem. The fix is to await every task, to add .catch() to every Promise, and to fail the request if the task it depended on rejected.
The promise chain that swallows rejections. await foo().catch(() => null) returns null if foo rejected, and the code that uses the result does not know that foo rejected. The fix is to return a typed error from the catch, not a silent null, and to handle the typed error explicitly at the call site.
The general pattern: a runtime error that is not propagated up the call stack is a runtime error that is not visible to the caller, the logs, or the monitoring. The team finds out about it from a customer, which is the worst possible time to find out.
The observability pattern
The fix for “we did not know about the runtime error until the customer told us” is observability — logs, traces, and errors in one place, queryable, with the context to debug from.
The minimum useful setup:
- Structured logs (JSON or logfmt) with a request ID, a user ID, the route, and the duration. Every request logs once at the start and once at the end. Every error logs the stack trace and the input that caused it.
- Error tracking (Sentry, Rollbar, Bugsnag, or a hosted equivalent) that captures uncaught exceptions with the same request ID, so the error is correlated with the logs.
- Tracing (OpenTelemetry, Jaeger, Honeycomb, or a hosted equivalent) that shows the request across services, so a runtime error in the database call is visible as part of the request, not as a separate event.
- Health checks at
/healthand/readyso the platform knows the service is alive, and adependencieshealth check that verifies the database, the cache, and the third-party APIs are reachable. The health check reference covers the difference between liveness and readiness.
The point is not to have all four. The point is to have the three that match the team’s budget and the application’s complexity. A simple service with structured logs and error tracking is enough. A distributed system with logs, traces, and errors is the minimum.
The post-mortem pattern
Every runtime error that reaches production is a fix-or-document decision. Either the team fixes the code so the error stops happening, or the team documents the error so the next person to see it knows what to do. The middle ground — “we know about it, we will fix it later” — is where the most runtime errors accumulate.
The pattern:
- The error appears in the monitoring.
- The on-call person triages: is this a fire, or is this a known error?
- If it is a known error, the runbook says what to do. Do that.
- If it is a new error, fix it now or file a ticket with a deadline.
- After the fix lands, the runbook updates. The error becomes a known error.
The reason this matters: a runtime error that has happened once will happen again, usually at the worst possible time. The first time is the cheapest time to fix it. The second time costs the team’s sleep. The third time costs a customer.
How this fits the rest of the stack
A service that catches runtime errors, logs them, and surfaces them to the team in one place is a service that ships. The RunxBuild hosting calculator is the right place to model what that costs — pick the runtime size, the log retention, the error tracking tier, and the database connections, and the calculator shows what the platform actually costs at the team’s actual usage.
Useful related references:
FAQ
What is the difference between a runtime error and a syntax error?
A syntax error is caught at parse time — the code never runs. A runtime error passes the parser and the type checker, the code starts running, and fails at execution. The diagnostic for a syntax error is the parser message; the diagnostic for a runtime error is the stack trace.
What is the difference between a runtime error and a logic bug?
A runtime error throws an exception or returns an error code, and there is a stack trace or a log line. A logic bug runs to completion and returns the wrong value, and there is no error to catch. Runtime errors are easier to debug because there is a concrete artifact; logic bugs require reasoning about the code.
What is the most common runtime error?
In JavaScript, TypeError: Cannot read property 'X' of undefined is the most common, by a wide margin. In Python, TypeError and KeyError together account for the majority. In Java, NullPointerException is the canonical one. The pattern is the same in all three: the code tried to use a value that was not there.
Why do runtime errors only happen in production?
They do not, in the literal sense, but they get noticed in production. A null dereference that happens once a month on a malformed input may never happen in dev, may not show up in the test suite, and may not be caught by static analysis. Production has more inputs, more users, and more time, so the rare path gets exercised.
What is an unhandled promise rejection?
In JavaScript, a Promise that rejects without a .catch() is “unhandled.” The runtime logs a warning, the function that returned the Promise returns undefined, and the rejection is invisible to the caller. The fix is to add .catch() (or to wrap the await in a try/catch) and to log the rejection.
How do I add observability to a service that does not have any?
Start with structured logs and a request ID. The next runtime error will be one line in the log with the stack trace, the request ID, and the user ID. That is enough to debug from. Once the logs are useful, add error tracking. Once the errors are tracked, add tracing if the system is distributed.
Should I add a try/catch around every function?
No. A try/catch that does not handle the error is worse than no try/catch, because the catch hides the error from the rest of the code. Add try/catch at the boundary where the code can do something useful with the error — log it, return a typed error, retry — and let the rest of the code throw naturally.