The difference is that the latter appeared in DOS 2.0.
MS-DOS 1.x was pretty much a rebranded version of Seattle Computer Products’ 86-DOS (initially named QDOS), which in turn was heavily inspired by CP/M. One of the design goals of 86-DOS has been to maintain a certain level of compatibility with CP/M-80: specifically, to be able to port CP/M software to DOS by translating 8080 assembly code into 8086/8088 assembly code with minimal amount of patching. Intel took care of translating the instructions themselves in their 8086 design; the part that remained was to supply a compatible operating system interface.
In CP/M-80, exiting the program was achieved by jumping to address zero, at the very beginning of the address space; this lay in the so-called zero page, where CP/M maintained an interface to the operating system. Address zero contained an instruction which jumped to the appropriate CP/M system call entry point which terminated the program. The equivalent data structure in DOS, the Program Segment Prefix, contains an
int 0x20 instruction at offset zero; with that instruction in place, jumping to offset 0 in the program’s segment exited the program just like jumping to absolute address 0 did in CP/M. In either operating system, the same entry point could be reached just by executing the return instruction; this is because the stack was initialised to contain a zero word at the bottom, so that returning would jump to that same zero address.0
With MS-DOS version 2.0, Microsoft started drawing a lot of design inspiration from Unix, specifically Xenix: they added features like hierarchical directory structure, file handles, I/O redirection, environment variables and process exit codes. Since the CP/M-style exit call did not accept an exit code, a new system call had to be created: that system call is interrupt 0x21 function 0x4c.1 The old exit call was of course kept, and made to return an exit code of zero.
With the right DOS version, either call can theoretically be used in either type of executable.2 The only restriction is that the
int 0x20 call requires that the
cs register point to the Program Segment Prefix. This is set up on startup in
.COM files (and usually maintained afterwards), but usually not in MZ (‘
.EXE’) executables; in multi-segment executables this requirement in practice forces the program to perform this interrupt call by jumping to the PSP. The interrupt 0x21 function 0x4c call, on the other hand, has no such requirement. Perhaps this difference is why your attempts to use the former system call failed.
0 The exit function was also available through CP/M’s generic system call mechanism,
call 5, as system call number zero. The preferred system call interface on DOS was interrupt 0x21, but the
call 5 interface was also supported by DOS, later becoming the source of endless headaches.
1 It was introduced along with a pair of other process-management system calls: "EXEC" (0x4b) and "WAIT" (0x4d); they enabled any program to launch executable files and check the exit code afterwards. Before DOS 2.0, loading programs was actually the responsibility of the shell, COMMAND.COM; any other program had to implement executable loading on its own.
2 DOS 1.x, on the other hand, did support
.EXE files, but of course only had the CP/M-style exit call, so it obviously had to work with those.