Blog › ICP guides

QBasic developer on retainer: global variable scope, GOSUB subroutines, GW-BASIC legacy, and BASIC on monthly retainer

October 2, 2026 · ~13 min read

A QBasic developer was maintaining a DOS billing application for a small accounting firm. The application generated invoices by accumulating line-item amounts into a running total, then calling a GOSUB subroutine labeled CALC.TOTAL to compute taxes and finalize the invoice. The main program loop used a variable named TOTAL to accumulate the invoice grand total across all line items. The CALC.TOTAL subroutine also used a variable named TOTAL as its working accumulator, initialized at the start of the subroutine with TOTAL = 0.

In classic BASIC — including QBasic — all variables are global. GOSUB subroutines do not create a new scope. There are no local variables in GOSUB blocks; every variable name in a GOSUB shares the same namespace as every other variable in the program. When the billing loop called GOSUB CALC.TOTAL after accumulating the line items, the first line of the subroutine executed TOTAL = 0, resetting the invoice grand total that the calling loop had just built. The subroutine then computed its own sum in TOTAL; when it returned, the outer variable TOTAL held the subroutine’s result rather than the accumulated invoice total. Three invoices with multiple line items printed a grand total that reflected only the subroutine’s final computation, not the sum of all line items. Wrong invoice totals: 3 → 0 after renaming the subroutine’s working variable to SUBTOTAL.

QBasic overview: the introduction to programming for a generation (1964–1991)

BASIC (Beginner’s All-purpose Symbolic Instruction Code) was created in 1964 at Dartmouth College by John Kemeny and Thomas Kurtz. The design goal was to make programming accessible to students with no prior computing experience: BASIC had simple syntax, no separate compilation step (it was interpreted line by line), and required no understanding of hardware addressing. Dartmouth BASIC ran on a time-sharing system; students typed programs at terminals and received responses within seconds. The interactivity and simplicity made BASIC the teaching language of the 1960s and 1970s.

Microsoft purchased a BASIC interpreter license in 1975 and wrote their own implementation (Microsoft BASIC) that ran on the Altair 8800 and became the foundation of BASIC on virtually every personal computer of the late 1970s and early 1980s. GW-BASIC (included with MS-DOS 3.x) and BASICA (IBM PC BASIC) were the standard BASIC interpreters on the IBM PC. QuickBASIC (1985) added a compiler, structured control flow (DO...LOOP, FUNCTION...END FUNCTION, SUB...END SUB), and an IDE. QBasic (1991, bundled with MS-DOS 5.0) was a subset of QuickBASIC without the standalone compiler; it included the same IDE and the same structured control flow but produced interpreted P-code rather than native executables. QB64 is a modern open-source implementation that compiles QBasic-compatible code to C++ for cross-platform execution.

BASIC retainer work today covers applications built in the 1980s and early 1990s that are still running on DOS systems, DOSBox, or have been forward-ported to QB64. The programs include billing systems, payroll utilities, inventory trackers, scientific data processors, and small-business accounting applications that were built when BASIC was the affordable, accessible alternative to the Pascal and C compilers that cost hundreds of dollars and required assembly-level debugging skills. The reason they persist: they work, the people who built them are often no longer available, and the organizations that depend on them lack the resources for a complete rewrite. Retainer work is the only way to keep them running while balancing the cost of maintenance against the cost of replacement.

BASIC retainer work specifically covers: global variable scope collision diagnosis in GOSUB subroutines; FUNCTION and SUB migration to replace GOSUB blocks with properly scoped procedures; line number and label migration from GW-BASIC numeric line numbers to QBasic labels; file I/O maintenance for OPEN/CLOSE/PRINT #n/INPUT #n/GET/PUT operations; and data type management for BASIC’s implicit type suffix system. Each category produces correctness bugs that are invisible without understanding BASIC’s specific design decisions — particularly the total absence of local scope in GOSUB subroutines.

Global variable scope and GOSUB subroutines in QBasic and GW-BASIC

Classic BASIC has one namespace for all variables in a program. A variable named TOTAL in one part of the program is the same variable as TOTAL anywhere else. This is not a design oversight — it was an intentional simplification for a language designed to be taught to beginners in one session. Variable names do not need to be declared; they come into existence when they are first assigned a value. The scope of every variable is the entire program.

GOSUB is BASIC’s original subroutine mechanism. GOSUB label transfers execution to the labeled line; RETURN transfers execution back to the line after the GOSUB. The subroutine body is just more program code, not a separate scope. Any variable read or written inside the subroutine is the same variable read or written in any other part of the program. There is no mechanism to declare a variable as local to a GOSUB block, because GOSUB is not a block — it is a conditional jump and return. Code inside a GOSUB has no awareness that it is inside a subroutine from the perspective of the variable system.

The practical consequence: any GOSUB subroutine that uses a common variable name (TOTAL, COUNT, I, SUM, RESULT, TEMP, X) risks colliding with a variable of the same name used in the calling code or in any other GOSUB called before or after it. The collision is silent: the assignment inside the subroutine overwrites the caller’s value, the caller’s subsequent access returns the subroutine’s value, and the wrong output follows without any error. In GW-BASIC programs, where every subroutine is a GOSUB because there is no FUNCTION or SUB syntax, every large program has multiple GOSUB subroutines sharing the global namespace. The longer the program and the more subroutines it has, the more likely a name collision is.

QuickBASIC (1985) and QBasic (1991) added FUNCTION...END FUNCTION and SUB...END SUB blocks with true local scope. Variables declared inside a FUNCTION or SUB block are local; they do not collide with variables of the same name outside the block. Parameters are passed by reference by default (modifying a parameter modifies the actual argument) or by value when enclosed in extra parentheses. Migrating GOSUB subroutines to SUB blocks eliminates global scope collisions, but the migration requires identifying every variable in the GOSUB that was intentionally shared with the calling code (as a substitute for parameter passing) and converting those to parameters or to explicitly SHARED declarations.

FUNCTION and SUB blocks, file I/O, and data type management in QBasic

The FUNCTION...END FUNCTION block in QBasic defines a named procedure that returns a value. Variables inside the block are local unless explicitly declared SHARED. The function returns its value by assigning to the function name inside the body: FUNCTION TotalInvoice(amount AS SINGLE, taxRate AS SINGLE) followed by TotalInvoice = amount * (1 + taxRate). Parameters are typed with the AS type clause; without an AS clause, the type is inferred from the variable name suffix (% for INTEGER, & for LONG, ! for SINGLE, # for DOUBLE, $ for STRING). The SUB...END SUB block defines a procedure that does not return a value but can modify parameters passed by reference.

QBasic file I/O uses the OPEN statement with a file mode: OPEN filename FOR INPUT AS #1 opens for sequential text reading; OPEN filename FOR OUTPUT AS #1 opens for sequential text writing; OPEN filename FOR APPEND AS #1 appends to an existing text file; OPEN filename FOR BINARY AS #1 opens for random byte access; OPEN filename FOR RANDOM AS #1 LEN = record_length opens a fixed-record binary file. The most common file I/O maintenance bug: a RANDOM file opened with a record length that no longer matches the actual record size after a data structure change. PUT #1, n, record and GET #1, n, record use the record length specified in the OPEN statement to seek to the correct byte offset. If the specified record length is wrong, all records after the first are read or written at wrong offsets. Diagnosis requires computing the actual record size from all fields in the TYPE...END TYPE declaration and comparing it to the LEN = value in the OPEN statement.

QBasic’s data type system uses either explicit suffix characters or AS type declarations. An undeclared variable in QBasic is SINGLE (32-bit floating-point) by default. Arithmetic between INTEGER (16-bit) and SINGLE values implicitly converts the INTEGER to SINGLE before the operation. The most common type management bug: a loop counter declared as INTEGER% that is compared to a SINGLE value. BASIC widens the integer to single for the comparison; if the single value has accumulated floating-point error, the comparison may be unexpectedly equal or not-equal. For counters that should be exact integers, using LONG& or explicitly adding DEFINT A-Z at the top of the program (which makes all undeclared variables INTEGER) prevents implicit widening and makes integer comparisons exact.

Typical QBasic retainer work and what it looks like in a work log

Global scope collision is the largest category of QBasic retainer work that produces no visible error. A GOSUB subroutine that uses a common variable name has been running correctly for every case where the calling code did not have a same-named variable with a value set before the call, or where the subroutine happened to reinitialize the variable to the same value the caller expected. The collision only manifests when the specific execution path sets a variable in the calling code that the GOSUB then clobbers. Work log entry: “CALC.TOTAL GOSUB label: variable TOTAL used as working accumulator; TOTAL = 0 at line 1 of subroutine reset outer invoice accumulator; main loop had accumulated TOTAL = 487.50 across 3 line items; GOSUB reset to 0 and recomputed from subroutine-only data; printed total: $12.75 (subroutine-only result) instead of $500.25 (full invoice); wrong invoices: 3 (any invoice with more than 1 line item); fix: renamed subroutine variable to SUBTOTAL; added comment: GOSUB variables are global — use unique names; wrong invoices before: 3; after: 0; 3h.”

RANDOM file record length mismatch is the second category. Adding a field to a TYPE...END TYPE record changes the record size; the LEN = value in the OPEN statement is not automatically updated. Work log entry: “InvoiceData.bas: InvoiceRec TYPE field Notes AS STRING * 40 added; old record size: 120 bytes; new record size: 160 bytes; OPEN "invoices.dat" FOR RANDOM AS #1 LEN = 120 not updated; GET #1, n, rec read at wrong offsets for all records after first; wrong customer names in all 340 records; fix: updated LEN = 160 in OPEN statement; rewrote invoices.dat with migration script; wrong records before: 340; after: 0; 6h.”

Type conversion precision loss is the third category. An INTEGER loop counter compared to a SINGLE computed from floating-point arithmetic produces an off-by-one loop termination. Work log entry: “ProcessBatch GOSUB: loop counter I% (INTEGER) compared to TARGET! (SINGLE, computed as BATCH.SIZE! * 0.1); for BATCH.SIZE! = 100.0, TARGET! = 10.000001 due to floating-point representation; loop terminated at I% = 10 because 10 < 10.000001 was true — wait, actually this loops one extra time; for BATCH.SIZE! = 30.0, TARGET! = 3.0000002; I% = 3 compared to 3.0000002 — loop ran 4 times instead of 3; wrong output records: 4; fix: changed comparison to I% >= CINT(TARGET!) to snap to nearest integer; wrong iterations before: 4; after: 0; 4h.”

Track QBasic developer retainer hours without the status emails

When a 3-hour session diagnoses a GOSUB global scope collision where a subroutine’s TOTAL = 0 reset the invoice accumulator built by the calling loop — because QBasic GOSUB subroutines share the same global variable namespace with no local scope — the work log needs to name the GOSUB label, the variable that collided, the scope mechanism, and the wrong-invoice count. HourTab gives your QBasic retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the BASIC mechanism. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks QBasic developer retainer hours

QBasic retainer work is invisible by the same mechanism that makes GOSUB subroutines functional for programs with careful variable naming: the collision only occurs when a specific variable name is reused between the subroutine and the calling code, and the subroutine happens to reset or overwrite the caller’s value. An invoicing program that processes hundreds of single-line-item invoices correctly may have the collision bug in its multi-line-item path that is never exercised during routine use. The wrong total only surfaces when a customer with three line items sees an invoice with an obviously wrong grand total. The connection between a TOTAL = 0 initialization inside a GOSUB and a wrong invoice total requires understanding that QBasic GOSUB subroutines are not scoped blocks — they are labeled sections of a flat program with a shared global variable table.

The work log needs to name the mechanism: which GOSUB label, which variable collided, how the collision reset or modified the calling code’s value, what the observable wrong output was, and what the fix was. A log entry that says “fixed invoice total bug, 3h” is not auditable. A log entry that says “CALC.TOTAL GOSUB: TOTAL initialized to 0 at line 1 of subroutine; outer TOTAL accumulated $487.50 across 3 line items before GOSUB call; subroutine reset outer TOTAL to 0; printed total was subroutine-only result; wrong invoices: 3; renamed subroutine variable to SUBTOTAL; wrong invoices before: 3; after: 0; 3h” is auditable.

HourTab gives QBasic developers a public retainer-hours URL they send to clients — accounting firms running DOS billing systems built in the early 1990s, small manufacturers using inventory applications written in GW-BASIC, and service businesses maintaining scheduling and payroll utilities that have been running since MS-DOS 5.0 shipped with QBasic in 1991. For QBasic retainers, each work log entry should name the BASIC mechanism: which GOSUB label, which variable scope collision, which file record mismatch. Comparative context: QBasic retainer work has conceptual overlap with other environments where variable scope requires explicit management — Turbo Pascal (another DOS-era language whose unit structure and typed files have analogous maintenance challenges); Visual Basic (BASIC’s Windows successor, where module-level and form-level variable scope introduced similar visibility bugs in VB6 applications); and COBOL (another legacy business language whose WORKING-STORAGE accumulator and PERFORM scope are the mainframe equivalent of GOSUB global state). QBasic is uniquely positioned as the language for the specific DOS-era small-business applications that were built by non-professional programmers and that have no team, no documentation, and no replacement budget.

FAQ: QBasic developer retainers

What does a QBasic developer on retainer typically do?

A QBasic developer on monthly retainer covers global variable scope diagnosis (all variables global in GOSUB subroutines; name collisions produce wrong values without error; identifying and renaming colliding variables); FUNCTION and SUB migration (QuickBASIC/QBasic FUNCTION...END FUNCTION and SUB...END SUB have true local scope; migrating GOSUB blocks eliminates global collisions); line number and label migration (GW-BASIC numeric line numbers to QBasic labels; GOTO/GOSUB to numeric line migration); file I/O maintenance (OPEN/CLOSE/PRINT #n/INPUT #n/GET/PUT; RANDOM file record length maintenance; binary vs text mode; fixed-length STRING types in TYPE records); and data type management (% INTEGER, & LONG, ! SINGLE, # DOUBLE, $ STRING suffixes; DEFINT/DEFLNG directives; implicit widening in arithmetic expressions).

What QBasic work is most commonly underlogged?

Global scope collision in GOSUB subroutines is the most underlogged: all variables are global in QBasic; a GOSUB subroutine that initializes a variable at the start of its body silently overwrites the calling code’s variable of the same name; the calling code’s accumulated value is reset; the wrong output only surfaces when the calling code has set the variable before the GOSUB call; fix is renaming the subroutine variable to a unique name, or migrating GOSUB to SUB...END SUB; 2 to 5 hours. RANDOM file record length mismatch: a TYPE field addition changes record size; LEN = in OPEN not updated; GET/PUT at wrong offsets; fix is update LEN and rewrite data file; 5 to 8 hours. Type conversion precision loss: INTEGER counter compared to SINGLE computed from floating-point; off-by-one loop termination; fix is CINT() snap; 2 to 4 hours.

What are typical QBasic developer retainer rates?

Entry-level QBasic developers with 1 to 2 years covering GOSUB scope debugging, global scope management, and basic file I/O maintenance typically bill at $45 to $80 per hour. Mid-level QBasic programmers with 2 to 4 years covering FUNCTION/SUB migration, line number conversion, binary file format maintenance, and QB64 modernization typically bill at $70 to $130 per hour. Senior QBasic and QuickBASIC developers with 4 or more years covering large DOS application architecture, BASIC-to-modern-language migration planning, and complex file I/O system maintenance typically bill at $100 to $180 per hour. Monthly retainer ranges: $500 to $1,200 per month for advisory engagements (6 to 12 hours per month); $1,200 to $4,000 per month for active application maintenance and modernization work.

What should a QBasic developer retainer agreement include?

A retainer agreement should specify: BASIC version scope (GW-BASIC, QBasic, QuickBASIC 4.5, QB64, PowerBASIC — each has different scope rules, file handling, and compilation targets; GW-BASIC is interpreted with numeric line numbers; QBasic is a subset of QuickBASIC without the standalone compiler; QB64 is a modern cross-platform implementation); scope management clarification (whether GOSUB subroutines use global scope or the codebase uses FUNCTION/SUB blocks; global scope is the principal maintenance liability); file I/O scope (whether binary record file maintenance is in scope; fixed-length string types in TYPE records; file format compatibility with downstream systems); FUNCTION/SUB migration scope (whether GOSUB-to-SUB migration is in scope; migration eliminates scope collisions but requires testing code that relied on intended global state sharing); and hour logging format (the GOSUB label, the variable that collided, the scope mechanism, the fix applied, and the wrong-output count before and after).

How should QBasic developer retainer hours be logged?

Log each QBasic retainer session with: the GOSUB label or FUNCTION name where the bug appeared (e.g., CALC.TOTAL GOSUB label); the variable that collided with calling scope (e.g., TOTAL used in both CALC.TOTAL GOSUB and the main invoice accumulation loop); the scope mechanism (e.g., QBasic global scope — all variables shared between GOSUB and calling code; TOTAL = 0 inside subroutine reset the outer TOTAL that had accumulated $487.50 across 3 line items); the fix applied (e.g., renamed subroutine variable to SUBTOTAL; added scope comment); and the wrong-output count before and after. For RANDOM file bugs: the OPEN statement, the old and new record sizes, the wrong-offset behavior, and the count of corrupt records. For type conversion bugs: the variable types, the arithmetic expression, the implicit conversion, and the fix applied.