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
1Public Function PostPayment(ByVal acct As String, _2 ByVal amt As Currency) As Boolean3 On Error Resume Next4 Dim rs As ADODB.Recordset5 Dim bal As Currency67 Set rs = gConn.Execute( _8 "SELECT Balance FROM Accounts WHERE AcctNo = '" _9 & acct & "'")10 bal = rs!Balance fails if rs.EOF: bal stays 01112 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 True17End Function
Modern: Explicit exceptions and result types
1public async Task<PostResult> PostPaymentAsync(2 string acct, decimal amt)3{4 await using var tx =5 await _db.BeginTransactionAsync();67 decimal? bal = await _accounts8 .GetBalanceAsync(acct, tx);9 if (bal is null)10 return PostResult.AccountNotFound;1112 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
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.
Related patterns
Usually found in the same codebase.
- VB6High
VB6 global module state
Maps to Explicit request context and injected services
- .NETHigh
WebForms ViewState and code-behind logic
Maps to SPA front end over a stateless API that rereads authoritative data
- T-SQLHigh
T-SQL dynamic SQL rules
Maps to Parameterised query with criteria as a reviewed decision table
- PL/SQLCritical
PL/SQL trigger side effects
Maps to Explicit domain operation with events via an outbox
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.