Skip to content
PYD ENTERPRISES

VB6 anti-pattern

VB6 On Error Resume Next

An error handler that swallows every runtime error and continues at the next statement, so failed reads leave stale or default values that later statements write to the database.

FAMILY
VB6
CUTOVER RISK
Critical
Changes runtime behavior in ways a line-by-line reading misses. Resolve before any cutover plan.
MODERN MAPPING
Explicit exceptions and result types
GENERATED ARTIFACT
findings.resume-next.json

Before and after

legacy source → modern mapping

Legacy: exhibits the pattern

modPayments.basVB6
1Public Function PostPayment(ByVal acct As String, _
2 ByVal amt As Currency) As Boolean
3 On Error Resume Next
4 Dim rs As ADODB.Recordset
5 Dim bal As Currency
6
7 Set rs = gConn.Execute( _
8 "SELECT Balance FROM Accounts WHERE AcctNo = '" _
9 & acct & "'")
10 bal = rs!Balance fails if rs.EOF: bal stays 0
11
12 gConn.Execute "UPDATE Accounts SET Balance = " _
13 & (bal - amt) & " WHERE AcctNo = '" & acct & "'"
14 gConn.Execute "INSERT INTO Ledger (AcctNo, Amt) " _
15 & "VALUES ('" & acct & "', " & -amt & ")"
16 PostPayment = True always True
17End Function

Modern: Explicit exceptions and result types

PaymentService.csC#
1public async Task<PostResult> PostPaymentAsync(
2 string acct, decimal amt)
3{
4 await using var tx =
5 await _db.BeginTransactionAsync();
6
7 decimal? bal = await _accounts
8 .GetBalanceAsync(acct, tx);
9 if (bal is null)
10 return PostResult.AccountNotFound;
11
12 await _accounts.SetBalanceAsync(acct, bal.Value - amt, tx);
13 await _ledger.AppendAsync(acct, -amt, tx);
14 await tx.CommitAsync();
15 return PostResult.Posted;
16}

Why it is dangerous in modernization

On Error Resume Next tells Visual Basic to ignore a runtime error and continue with the statement after the one that failed. It stays in force for the rest of the procedure, or until another On Error statement replaces it. Errors raised in called procedures that have no handler of their own propagate back to the caller and are swallowed there, with execution continuing after the call. A failed recordset read leaves a variable at its previous value or at its default (0, empty string, Empty), and the next statement uses that value. In financial VB6 applications this produces real defects, such as setting a balance to the negative of a payment when the account lookup failed. Ported code with ordinary exception handling will throw where the legacy code continued, which is correct but changes behavior, and someone has to know where.

How PYD ENTERPRISES detects it

The VB6 front end parses forms, modules and classes (.frm, .bas, .cls) and the project file, then computes for each statement the error-handling mode in effect: none, GoTo label, or Resume Next. Procedures without a handler are marked as propagating, and the call graph extends each caller's mode to the calls it makes. Statements that can raise (recordset field access, type conversions, COM calls, file I/O, division) inside a Resume Next region are flagged, and dataflow analysis follows the variables they assign to any later write: database updates, file writes, and return values. Checks of Err.Number after a statement are recognised as deliberate handling and lower the finding's severity.

Maps to: Explicit exceptions and result types

Each swallowed error becomes either a typed exception or an explicit result value, decided per statement from the evidence: a missing account becomes a NotFound result, a database failure becomes an exception that rolls back the transaction. Where the legacy behavior relied on the default value (an optional field treated as zero), the new code says so with an explicit default rather than an error being ignored.

Generated specification fragment

findings.resume-next.jsonJSON
1{
2 "procedure": "modPayments.PostPayment",
3 "errorMode": "Resume Next (line 3 to end)",
4 "findings": [
5 {
6 "line": 10,
7 "statement": "bal = rs!Balance",
8 "canRaise": "3021 no current record (EOF)",
9 "consequence": "bal keeps 0; line 12 writes Balance = -amt",
10 "severity": "critical"
11 },
12 {
13 "line": 16,
14 "statement": "PostPayment = True",
15 "consequence": "caller cannot detect any failure above",
16 "severity": "high"
17 }
18 ]
19}

Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.

Error 3021 and the negative balance

When no account matches, the recordset is at EOF and reading rs!Balance raises error 3021 (either BOF or EOF is True, or the current record has been deleted). Resume Next skips the assignment. Because bal was just declared, it holds 0, and the UPDATE sets the balance to minus the payment amount. The INSERT then writes a ledger row for an account that does not exist, and the function returns True. Every step is visible in the code once you know to read it under the error mode, which is exactly the reading that a deterministic engine does on every statement and a human reviewer does on some of them.

Deliberate uses

Not every Resume Next is a defect. VB6 code commonly uses it to test whether a collection key exists, or to try an optional COM object, immediately checking Err.Number afterwards. The engine recognises the pattern of a single raising statement followed by an Err check and classifies it as plumbing, mapping it to a TryGetValue style call or a null check in C#. Findings are reserved for raising statements whose failure flows, unchecked, into later behavior.

Planning the behavior change

  • List every Resume Next region with flagged statements and the writes they reach.
  • For each, decide: reject (exception), report (result type), or keep the default (explicit).
  • Generate fixtures for the failing case so the decision is tested, not assumed.
  • Expect some 'new' errors after cutover; they are old failures that used to be silent. Reconcile the legacy data they left behind, such as negative balances or orphan ledger rows, before migration.

Find every VB6 On Error Resume Next in your codebase.

Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.