INT 21h AH=40h writes CX bytes from DS:DX at the file pointer and moves the pointer past them. Writing past the end makes the file longer; writing in the middle overwrites what was there. AX returns the number of bytes written.

Check AX, not only CF. When the disk fills up DOS writes what fits and returns CF = 0 with a smaller AX, so a program that tests only CF loses data quietly. CF = 1 with 05h means the handle was opened read-only, 06h that the handle is not open.

Standard handles

BXGoes toNotes
1standard outputthe screen, or a file after PROG > OUT.TXT
2standard errorthe screen; COMMAND.COM cannot redirect it
3AUXthe first serial port
4PRNthe first printer port

Writing to handle 1 is the general way to print. Unlike INT 21h AH=09h it takes a length instead of looking for a terminator, so the text may contain "$", and it can be up to 64 KB long:

prints two lines: "a $ sign is just a byte here" and "and this went to handle 2"
MOV AH, 40h ; DOS: write to a handle
MOV BX, 1 ; 1 = standard output
MOV CX, MSG1L ; a length: 40h does not look for "$"
MOV DX, OFFSET MSG1
INT 21h
MOV AH, 40h
MOV BX, 2 ; 2 = standard error
MOV CX, MSG2L
MOV DX, OFFSET MSG2
INT 21h
MSG1 DB "a $ sign is just a byte here", 13, 10
MSG1L EQU $ - MSG1
MSG2 DB "and this went to handle 2", 13, 10
MSG2L EQU $ - MSG2

Writing 0 bytes

CX = 0 writes nothing and sets the length of the file to the position of the file pointer. With the pointer inside the file the rest is cut off; with the pointer moved past the end by INT 21h AH=42h, the file grows to that length (DOS gives it the clusters but does not clear them, so the new bytes are whatever was on the disk). It is the only way DOS offers to shorten a file without recreating it.

prints: size after the 0-byte write: 5, contents: Hello
MOV AH, 3Ch ; DOS: create TEST.TXT
XOR CX, CX ; CX = 0: normal attributes
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK ; stop if CF = 1
MOV BX, AX ; BX = the handle
MOV AH, 40h ; DOS: write 12 bytes
MOV CX, 12
MOV DX, OFFSET HELLO
INT 21h
CALL CHECK
MOV AX, 4200h ; file pointer back to offset 5
XOR CX, CX
MOV DX, 5
INT 21h
CALL CHECK
MOV AH, 40h ; write 0 bytes: the file ends here now
XOR CX, CX
INT 21h
CALL CHECK
MOV AX, 4202h ; to the end: DX:AX = the length
XOR CX, CX
XOR DX, DX
INT 21h
CALL CHECK
MOV SI, AX ; SI = the length
MOV AH, 3Eh ; close, then open again to read it
INT 21h
MOV AX, 3D00h ; DOS: open TEST.TXT for reading
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK
MOV BX, AX ; BX = the new handle
MOV AH, 3Fh ; read what is left
MOV CX, 12
MOV DX, OFFSET BUF
INT 21h
CALL CHECK
MOV DI, AX ; DI = bytes read
MOV AH, 3Eh ; close the handle
INT 21h
MOV AH, 41h ; delete TEST.TXT
MOV DX, OFFSET FNAME
INT 21h
MOV DX, OFFSET M1
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AX, SI
CALL PDEC
MOV DX, OFFSET M2
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV CX, DI
MOV DX, OFFSET BUF
CALL OUT1
MOV DX, OFFSET CRLF
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
FNAME DB "TEST.TXT", 0
HELLO DB "Hello, world"
M1 DB "size after the 0-byte write: $"
M2 DB ", contents: $"
CRLF DB 13, 10, "$"
BUF DB 64 DUP (?)
CHECK PROC NEAR ; CF = 1: print the error code in AL and exit
JNC CHECK1
PUSH AX
MOV DX, OFFSET FAILMSG
MOV AH, 09h
INT 21h
POP AX
CALL PHEXB
MOV AX, 4C01h ; exit with code 1
INT 21h
CHECK1:
RET
CHECK ENDP
FAILMSG DB "failed: error $"
PHEXB PROC NEAR ; print AL as two hex digits
PUSH CX
PUSH DX
MOV DH, AL ; keep the byte
MOV CL, 4
SHR AL, CL ; the high digit first
MOV CX, 2
PHEXB1:
AND AL, 0Fh
ADD AL, "0"
CMP AL, "9"
JBE PHEXB2
ADD AL, 7 ; 10..15 -> "A".."F"
PHEXB2:
MOV DL, AL
MOV AH, 02h ; DOS: print DL
INT 21h
MOV AL, DH ; then the low digit
LOOP PHEXB1
POP DX
POP CX
RET
PHEXB ENDP
PDEC PROC NEAR ; print AX in decimal
PUSH BX
PUSH CX
PUSH DX
MOV BX, 10
XOR CX, CX ; CX = digits pushed
PDEC1:
XOR DX, DX
DIV BX ; AX = AX / 10, DX = the last digit
PUSH DX
INC CX
OR AX, AX
JNZ PDEC1
PDEC2:
POP DX ; the digits come back first to last
ADD DL, "0"
MOV AH, 02h
INT 21h
LOOP PDEC2
POP DX
POP CX
POP BX
RET
PDEC ENDP
OUT1 PROC NEAR ; write CX bytes at DS:DX to handle 1
PUSH BX
MOV BX, 1 ; standard output
MOV AH, 40h
INT 21h
POP BX
RET
OUT1 ENDP

The example closes the file and opens it again before reading it back. In DOSBox-X, on a drive that is a folder of the host (as C: is here), the new length shows at once, but a read through the same handle still returns the bytes that were cut off until the file is opened again; on real DOS the read stops at the new end either way.

On a device a write of 0 bytes does nothing. Handles 1 and 2 both lead to the CON device, which in the workbench feeds the DOS screen and the terminal alike.